Last active
August 3, 2026 19:54
-
-
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
Thanks for this, Dave. Your
SearchADDnsRecordgist is what got me started readingdnsRecorddirectly out of AD, and theADDnsNodeclass was the clearest example of that blob layout I found outside the spec.I've published a script based on the same idea:
https://github.com/Mike-Crowley/Public-Scripts/blob/main/AD_DS/Get-AdDnsNodeReport.ps1
I reimplemented the parsing inline so there's no runtime dependency, and added the ownership and permissions side: which computer owns each
dnsNode, and whether that owner can still write to its own record. That's usually what's behind records that never scavenge. You're credited in the script notes.Two things I ran into on the binary format, in case they're useful to anyone else here:
dnsRecordattribute putsReservedbeforeTimeStamp(MS-DNSP 2.3.2.2), the reverse of the RPCDNS_RPC_RECORDstruct. Parse it the RPC way and every record reads as static.DNS_COUNT_NAMEis null-terminated and itsLengthcounts the null, so walking the labels alone lands one byte short. That shows up on SOA, which carries two names back to back.Thanks again for putting this out there.