Last active
January 8, 2024 16:09
-
-
Save thedavecarroll/ea2b4f0ba7527bd469e59e1aabf3e0b0 to your computer and use it in GitHub Desktop.
This simple PowerShell script module uses a custom class and Get-ADObject to search an Active Directory-integrated DNS Zone by name or partial name.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#Requires -Version 5.1 | |
#Requires -Module ActiveDirectory | |
$script:ADRootDSE = Get-ADRootDSE | |
class ADDnsNode { | |
# AD Object Properties | |
[String]$Name | |
[String]$CanonicalName | |
[System.DateTime]$Created | |
[System.DateTime]$Modified | |
[String]$DistinguishedName | |
[System.Boolean]$IsDeleted | |
[System.Boolean]$IsTombstoned | |
[System.Boolean]$ProtectedFromAccidentalDeletion | |
# DnsRecord Properties | |
[System.UInt32]$UpdateAtSerial | |
[System.UInt32]$TTL | |
[System.UInt32]$Age | |
[Nullable[System.DateTime]]$Timestamp | |
[System.Boolean]$IsStatic | |
[String]$RRType | |
[String]$Data | |
hidden [Microsoft.ActiveDirectory.Management.ADObject]$OriginalADObject | |
# Constructors | |
ADDnsNode () { } | |
ADDnsNode ([PSCustomObject]$InputObject) { | |
$this.Name = $InputObject.Name | |
$this.CanonicalName = $InputObject.CanonicalName | |
$this.Created = $InputObject.Created | |
$this.Modified = $InputObject.Modified | |
$this.DistinguishedName = $InputObject.DistinguishedName | |
$this.IsDeleted = $InputObject.IsDeleted | |
$this.IsTombstoned = $InputObject.dNSTombstoned | |
$this.ProtectedFromAccidentalDeletion = $InputObject.ProtectedFromAccidentalDeletion | |
$this.OriginalADObject = $InputObject | |
$This.ConvertFromDnsRecord($InputObject.dnsRecord[0]) | |
} | |
# Methods | |
[void] ConvertFromDnsRecord([byte[]]$DnsRecord) { | |
try { | |
$TTLRaw = $DnsRecord[12..15] | |
$this.TTL = [BitConverter]::ToUInt32($TTLRaw, 0) | |
[void][array]::Reverse($TTLRaw) # reverse for big endian | |
$this.UpdateAtSerial = [BitConverter]::ToUInt32($DnsRecord, 8) | |
$RecordType = $null | |
switch ([BitConverter]::ToUInt16($DnsRecord, 2)) { | |
1 { | |
$RecordData = '{0}.{1}.{2}.{3}' -f $DnsRecord[24], $DnsRecord[25], $DnsRecord[26], $DnsRecord[27] | |
$RecordType = 'A' | |
} | |
16 { | |
[string]$RecordData = '' | |
[int]$SegmentLength = $DnsRecord[24] | |
$Index = 25 | |
while ($SegmentLength-- -gt 0) { | |
$RecordData += [char]$DnsRecord[$index++] | |
} | |
$RecordType = 'TXT' | |
} | |
{$_ -in 2,5,12} { | |
$RecordData = $this.GetName($DnsRecord[24..$DnsRecord.length]) | |
switch ($_) { | |
2 { $RecordType = 'NS' } | |
5 { $RecordType = 'CNAME' } | |
12 { $RecordType = 'PTR' } | |
} | |
} | |
default { | |
$RecordData = $([System.Convert]::ToBase64String($DnsRecord[24..$DnsRecord.length])) | |
switch ($_) { | |
6 { $RecordType = 'SOA' } | |
13 { $RecordType = 'HINFO' } | |
15 { $RecordType = 'MX' } | |
17 { $RecordType = 'RP' } | |
28 { $RecordType = 'AAAA' } | |
33 { $RecordType = 'SRV' } | |
default { $RecordType = 'UNKNOWN' } | |
} | |
} | |
} | |
$this.RRType = $RecordType | |
$this.Data = $RecordData | |
$this.Age = [BitConverter]::ToUInt32($DnsRecord, 20) | |
if ($this.Age -ne 0) { | |
$RecordTimestamp = ((Get-Date '01/01/1601 00:00:00').AddHours($this.Age)) | |
$RecordIsStatic = $false | |
} else { | |
$RecordTimestamp = $null | |
$RecordIsStatic = $true | |
} | |
$this.Timestamp = $RecordTimestamp | |
$this.IsStatic = $RecordIsStatic | |
} | |
catch { | |
'{0} : {1}' -f $This.Name,$This.DistinguishedName | Write-Error | |
$PSCmdlet.ThrowTerminatingError($_) | |
} | |
} | |
hidden [string] GetName([byte[]]$Raw) { | |
try { | |
[Int]$Segments = $Raw[1] | |
[Int]$Index = 2 | |
[String]$GetName = '' | |
while ($Segments-- -gt 0) { | |
[Int]$SegmentLength = $Raw[$Index++] | |
while ($SegmentLength-- -gt 0) { | |
$GetName += [Char]$Raw[$Index++] | |
} | |
$GetName += "." | |
} | |
return $GetName | |
} | |
catch { | |
$_ | Write-Warning | |
return '' | |
} | |
} | |
} | |
function Search-DnsRecord { | |
[CmdletBinding(DefaultParameterSetName='ByHostName')] | |
param( | |
[Parameter(Mandatory,ParameterSetName='ByHostName')] | |
[string]$HostName, | |
[Parameter(Mandatory,ParameterSetName='ByFilter')] | |
[string]$Filter, | |
[switch]$IncludeTombstoned, | |
[Parameter(Mandatory)] | |
[string]$ZoneName, | |
[string]$Server, | |
[ValidateSet('DomainDnsZones','ForestDnsZones')] | |
[string]$ADZoneType = 'DomainDnsZones' | |
) | |
$SearchBase = 'DC={0},CN=MicrosoftDNS,{1}' -f $ZoneName,($script:ADRootDSE.namingContexts.Where{$_ -match $ADZoneType})[0] | |
$SearchFilter = @() | |
# specify the Dns-Node objectCategory which is faster than objectClass | |
$DnsNodeCategory = 'CN=Dns-Node,{0}' -f $script:ADRootDSE.schemaNamingContext | |
$SearchFilter += ('objectCategory -eq "{0}"' -f $DnsNodeCategory) | |
# add tombstone filter, if required | |
if (-Not $PSBoundParameters.ContainsKey('IncludeTombstoned')) { | |
$SearchFilter += 'dNSTombstoned -eq $false' | |
} | |
# filter by name or partial name | |
switch ($PSCmdlet.ParameterSetName) { | |
'ByHostName' { | |
$SearchFilter += 'Name -eq "{0}"' -f $HostName | |
} | |
'ByFilter' { | |
if ($Filter -eq '*') { | |
$SearchFilter += 'Name -like "*"' -f $Filter | |
} else { | |
$SearchFilter += 'Name -like "*{0}*"' -f $Filter | |
} | |
} | |
} | |
$ADObjectParams = @{ | |
SearchBase = $SearchBase | |
Properties = '*' | |
Server = $Server | |
ResultSetSize = [int32]::MaxValue | |
Filter = $SearchFilter -join ' -and ' | |
} | |
Get-ADObject @ADObjectParams | ForEach-Object { | |
[ADDnsNode]::New($_) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
There are some record types that are not correctly decoded. I am working on this in my spare time.