Skip to content

Instantly share code, notes, and snippets.

@thedavecarroll
Last active January 8, 2024 16:09
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thedavecarroll/ea2b4f0ba7527bd469e59e1aabf3e0b0 to your computer and use it in GitHub Desktop.
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.
#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($_)
}
}
@thedavecarroll
Copy link
Author

Caveat
I have only used this for DomainDnsZones partition, but it should work for ForestDnsZones. Please let me know here if it does not.

@thedavecarroll
Copy link
Author

There are some record types that are not correctly decoded. I am working on this in my spare time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment