Daher nehme ich den Ball auf um einen der vielen Steine im Mosaik mit dem Namen „AD-Security“ zu zeigen. Namentlich geht es um den List Object Mode (LOM), welcher schon etwas älter* ist.
*ich meine seit AD 2003, muss aber noch mal recherchieren**
**Ja, mindestens seit 2003 R2 / 2003 SP1 (zur Erinnerung, das war im Jahr 2005).
List Object Mode und Security
Was hat das Ganze „List Object Mode“-Zeug mit On-Premises AD-Security zu tun?
Relativ einfach erklärt: nicht aufklärbar sein (siehe Punkt 4).
Von außen, z. B. als Domänen-Benutzer, sollte nicht erkennbar sein, wer ein hochprivilegiertes Konto ist. Um ein hochprivilegiertes Konto zu erkennen kann der Domänen-Benutzer, sofern versiert, eine PowerShell- oder LDAP-Abfrage machen. Wir haben es bei Active Directory mit einem „read many“-Verzeichnis zu tun.
Hierüber bekomme ich schnell eine Übersicht, wer administrativ unterwegs ist.
Administrative Konten sind für eine angreifende Partei mit Sicherheit attraktiver als unprivilegierte Konten.
Update zum Webinar-Termin, 25.04.2024
Discovery per ADSI
Für die Gruppe der „Built-in Administratoren“
$DN = ([adsisearcher]"(&(objectClass=group)(objectsid=S-1-5-32-544))").FindAll().properties.distinguishedname
([adsisearcher]"(&(memberOf:1.2.840.113556.1.4.1941:=$DN)(objectclass=user))").FindAll().properties.samaccountname
Für die Gruppe der „Domain Admins“
$DomainSID = (New-Object System.Security.Principal.SecurityIdentifier([byte[]](([adsi]"LDAP://cloud-hybrid.de").Properties."objectSID")[0],0)).toString()
$DomAdminRid = '-512'
$DomAdminObjectSID = $DomainSID + $DomAdminRid
$DN = ([adsisearcher]"(&(objectClass=group)(objectsid=$DomAdminObjectSID))").FindAll().properties.distinguishedname
([adsisearcher]"(&(memberOf:1.2.840.113556.1.4.1941:=$DN)(objectclass=user))").FindAll().properties.samaccountname
Discovery per PowerShell-Modul ActiveDirectory
Für die Gruppe der „Built-in Administratoren“
$AdminGroupSID = 'S-1-5-32-544'
$AdminGroup = Get-ADObject -Filter { objectSid -eq $AdminGroupSID }
(Get-ADGroupMember -Identity $AdminGroup.DistinguishedName -Recursive).sAMAccountName
Für die Gruppe der „Domain Admins“
$DomAdminGroupRID = '-512'
$DomainSID = (Get-ADDomain).DomainSID
$DomainAdminSID = $DomainSID.Value + $DomAdminGroupRID
$DomAdminGroup = Get-ADObject -Filter { objectSid -eq $DomainAdminSID }
(Get-ADGroupMember -Identity $DomAdminGroup.DistinguishedName -Recursive).sAMAccountName
List Object Mode (LOM)
Der List Object Mode, oder später hier mit „LOM“ abgekürzt, ist ein Feature des Active Directory, welches die Sichtbarkeit von Objekten steuert.
Active Directory läuft „out of the box“ im „list contents mode“. Das bedeutet, salopp formuliert, dass man sich schwer tut Objekte zu verstecken. Möchte ich beispielsweise die User in der OU OU=Users,OU=ADDS,OU=Tier0,OU=ESAE,DC=CLOUD-HYBRID,DC=DE verstecken, könnte man auf das Recht „List contents / Read all properties / Read permissions“ mit „Deny“ setzen.
Für ein Mitglied von Tier2-Allow sieht das dann aber komisch aus, Verstecken geht anders.
Was waren denn bisher die Strategien bezüglich administrativer Konten? Wie wurde bisher die Aufklärung dieser Konten verhindert?
Die drei bisherigen Strategien:
Um richtig zu verstecken brauchen wir den LOM. Ich empfehle den LOM seit 2013 um sensible AD-Objekte vor neugierigen Blicken zu schützen.
DSHeuristics!
Über die dSHeuristics wird der LOM aktiviert.
Die dSHeuristics ist ein Attribut, welches sich in der Configuration im Objekt Directory Services findet (CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,DC=<domain-name>,DC=<tld>).
Dieses Attribut, dSHeuristics, steht normaler Weise auf „<not set>“, zumindest „out of the box“.
Setzt man dSHeuristics auf „001“, wird der LOM aktiviert. Kein Neustart von DCs, Memberservern, Clients oder Diensten ist hier notwendig. Setzen, fertig.
Vorsicht!
Die dSHeuristics ist ein Attribut mit der Syntax „Big Endian“ und durch den Speicherort, Configuration, gilt eine solche Änderung im gesamten Forest!
Was bringt jetzt der LOM?
Eine neue Berechtigung!
„List object“
Daher auch der Name: List object mode.
Der LOM schaltet also „List object“ frei.
Das alleine reicht nicht.
„List object“ muss immer am Eltern-Objekt mit „List contents“ kombiniert werden.
Möchte ich verhindern, dass jemand ein Objekt sieht, muss am Elternobjekt das Recht „List contents“ entzogen werden und am eigentlichen Objekt das Recht „List object“ entzogen werden.
Die Gruppe „Authenticated Users“ hat immer „Read“ für „List contents“. Da ich nichts davon halte die vordefinierten Berechtigungen zu kürzen, füge ich lieber hinzu. Wenn auch, wie hier, mit der Notwendigkeit eines „Deny“.
SDProp / admincount=1
Der Fluch wird jetzt ein Segen!
Was habe ich nicht schon gestöhnt über den SDPropagator / SDProp, welcher in Verbindung mit dem PDCE die „geschützten Objekte“ schützt.
Nun jedoch wird der Fluch zum Segen: SDProp hilft uns den Schutz voll automatisch auszurollen! Noch drei ACL-Änderungen und wir sind fertig mit der Arbeit, die Admins versteckt und der Angreifer schaut hier mit den Ofenrohr ins Gebirge.
Wir müssen noch editieren:
- CN=Users,DC=CLOUD-HYBRID,DC=DE
- CN=Builtin,DC=CLOUD-HYBRID,DC=DE
- CN=AdminSDHolder,CN=System,DC=CLOUD-HYBRID,DC=DE
Bitte selbständig folgende Transferleistung erbringen: DC=CLOUD-HYBRID,DC=DE in den jeweils eigenen, passenden DN übersetzen, vielen Dank.
W wie weg
Nach spätestens 1 Stunde galloppiert der PDCE los und propagiert die neue ACL auf alle geschützten Objekte.
Das Ergebnis überzeugt:
Sicht des eines Administrators:
Sicht eines Tier2-Allow-Users:
„This demonstrates the value of not being seen.“
Security by obscurity kann nicht alleine die Lösung sein, schon klar.
Vielmehr sollte man man die Unsichtbarkeit vom Tier0, wie bereits oben genannt, als ein Stein im Mosaik „AD-Security“ betrachten.
So mal unter uns Gebetsschwestern: was sind denn da noch für Steine im Mosaik?
- Tiering implementieren: einem nackten Mann greift man nicht in die Tasche.
- krbtgt-Passwortänderungen durchführen: alle 12 Stunden
- Passwortänderungen der DC beschleunigen: jeden Tag muss geändert werden
- RC4 bei Kerberos V5 abschalten: siehe Blogartikel „Kerberoasting„
- Windows LAPS einführen: DSRM-Passwörter ändern lassen
- kürzere Kerberos-Ticketlaufzeiten implementieren
- nutzen der Gruppe Protected Users: siehe Blogartikel „Protected Users„
- Default Domain und Default Domain Controllers Policy anpassen: Stichworte Tiering und der Klassiker „Add workstations to domain“
Webinar zum dem Thema am 25.04.2024 ab 16:00 Uhr (UTC +2) – Dauer ca. 30 Minuten
Ich biete zu dem Thema ein kurzes, kostenfreies Webinar an:
- Wann: 25.04.2024, ab 16:00 Uhr (UTC +2)
- Dauer: ca. 30 Minuten
- Link: https://events.teams.microsoft.com
Nachtrag zum Webinartermin, 25.04.2024 – 16:00 Uhr
Im Webinar kam die Frage auf (vielen Dank dafür), wie man diesen „Sichtschutz“ für das Tier1 implementieren kann. Hier gibt es, meine Auffassung nach, zwei praktikable Möglichkeiten, die sich im logistischen Aufwand unterscheiden:
Lösung A: Tier1-Gruppen manuell mit admincount=1 markieren
Die Tier1-Gruppen werden manuell mit dem Attributwert „1“ beim Attribut „admincount“ versehen. Die Berechtigungen werden dann durch den PDC-E wie bei den Tier0-Konten propagiert. Die Berechtigungsvergabe erfolgt analog zu dem Schutz von Tier0, jedoch: die Tier1-Gruppen müssen bei den Berechtigungen in der OU-Struktur berücksichtigt werden.
Lösung B: Tier1-Gruppen manuell schützen
Wie Lösung A, zusätzlich muss die ACL bei jeder Tier1-Gruppe und bei jedem Tier1-User manuell gepflegt werden. Hierzu kann, beispielsweise, eine geplante Aufgabe / scheduled task herangezogen werden. Ein möglicher, überschaubarer Ansatz wäre es, über eine AD-Abfrage mit Namen, welche mit „T1“ beginnen, die ACL schreiben zu lassen.
Egal wie, ein Blick in die Bildergallerie sollte es verdeutlichen.
Letzte Anmerkung:
Will man das Tier0 vor dem Tier1 und Tier2 verbergen, so ist anstelle von Tier2-Allow die Gruppe Tier0-Deny geeignet.
Nachtrag zum 19.02.2025
Das Einrichten vom LOM geht mit einem PowerShell-Skript, welches ich geschrieben habe, etwas leichter.
Invoke-T0SecurityPrincipalHiding -T0HideGroup T0-Hide -TargetOU OU=Accounts,OU=Tier0,OU=ESAE,DC=cloud-hybrid,DC=de
beziehungsweise das Rückgängig machen:
Invoke-T0SecurityPrincipalHiding -T0HideGroup T0-Hide -TargetOU OU=Accounts,OU=Tier0,OU=ESAE,DC=cloud-hybrid,DC=de -Undo
Das Skript muss man sich kopieren und als *.ps1-Datei speichern (eg. T0-HideSecurityPrincipals.ps1). Danach wird dieses Skript ge-dot-sourced.
. .\T0-HideSecurityPrincipals.ps1
oder
. C:\temp\T0-HideSecurityPrincipals.ps1
Das Skript:
<# .SYNOPSIS Hides sensitive T0 objects .DESCRIPTION Security sensitive T0 objects from view of almost everyone by using the list object mode (LOM) in Active Directory. The dSHeuristics will be changed to 001 (if needed). 001 in the dSHeuristics activates the list object mode. Other changes to the attribute dSHeuristics will not be touched. Once implemented (first run of this script) will leave the LOM in place. It does not turn it off afterwards. The users container in Active Directory will be altered in it's DACL. The stated group will receive a DENY for ListChildren (List contents in the GUI). The object AdminSDHolder will be altered in it's DACL as well by stating a DENY for the given group for ListObject (List object in the GUI). The DACLs of AdminSDHolder will be propagated to all T0 objects in Active Directory every hour by the PDC-E. To make LOM work the permission DENY on ListChildren (List contents in the GUI) has to be set on every parent object of a T0 object. This would normally be a OU. Once those two permissions meet (DENY ListChilden at parent object + DENY ListObject at T0 objects) the T0 object will not be seen by a member of the T0HideGroup. Typically the T1-Allow, T2-Allow and other user specified groups will be nested in the T0HideGroup. The scipt has to runs as a member of the "Enterprise Admins" to alter the attribute dSHeuristics. CAREFULLY: DO NOT NEST "Domain Users" OR "Authenticated Users" IN THE T0HideGroup. .EXAMPLE This example hide the T0 object from members of the group "T0-Hide" at the OU 'OU=Accounts,OU=Tier0,OU=ESAE,DC=cloud-hybrid,DC=de' Invoke-T0SecurityPrincipalHiding -T0HideGroup T0-Hide -TargetOU OU=Accounts,OU=Tier0,OU=ESAE,DC=cloud-hybrid,DC=de .EXAMPLE This example hide the T0 object from members of the group "T0-Hide" at the OU 'OU=Accounts,OU=Tier0,OU=ESAE,DC=cloud-hybrid,DC=de' Invoke-T0SecurityPrincipalHiding -T0HideGroup T0-Hide -TargetOU OU=Accounts,OU=Tier0,OU=ESAE,DC=cloud-hybrid,DC=de .EXAMPLE This example hide the T0 object from members of the group "T0-Hide" at the OU 'OU=Accounts,OU=Tier0,OU=ESAE,DC=cloud-hybrid,DC=de' by using the alias HideT0 and the parameter positions HideT0 T0-Hide 'OU=Accounts,OU=Tier0,OU=ESAE,DC=cloud-hybrid,DC=de' .EXAMPLE This example hide the T0 object from members of the group "T0-Hide" at the OU 'OU=Accounts,OU=Tier0,OU=ESAE,DC=cloud-hybrid,DC=de' by using the alias StealthT0 and the parameter positions StealthT0 T0-Hide 'OU=Accounts,OU=Tier0,OU=ESAE,DC=cloud-hybrid,DC=de' .NOTES Version 1.0.0 - 2021/05/12 - Martin Handl - initial Version 1.0.1 - 2025/02/18 - Martin Handl - Bug with writing ACLs fixed + force ACL update on protected users .LINK https://iqunit.com/essential-active-directory-2025-workshop/ #>
function Invoke-T0SecurityPrincipalHiding
{
[CmdletBinding()]
[Alias("HideT0","StealthT0")]
param (
[Parameter(Position = 0, Mandatory = $true)]
[System.String]$T0HideGroup,
[Parameter(Position = 1, Mandatory = $false)]
[Switch]$Undo
)DynamicParam
{
# Set the dynamic parameters' name
$ParameterName = 'TargetOU'
# Create the dictionary
$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
# Create the collection of attributes
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
# Create and set the parameters' attributes
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$ParameterAttribute.Position = 1
# Add the attributes to the attributes collection
$AttributeCollection.Add($ParameterAttribute)
# Generate and set the ValidateSet
$arrSet = (Get-ADOrganizationalUnit -Filter { name -like "*" } -SearchBase (Get-ADDomain).distinguishedname -SearchScope Subtree).distinguishedname
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)
# Add the ValidateSet to the attributes collection
$AttributeCollection.Add($ValidateSetAttribute)
# Create and return the dynamic parameter
$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
$RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
return $RuntimeParameterDictionary
}
begin
{
# Probing Group
Write-Verbose "Probing Group..."
$T0HideGroup = Get-ADGroup -Identity $T0HideGroup -ErrorVariable GroupDoesNotExist -ErrorAction SilentlyContinue
if ($GroupDoesNotExist.length -gt 0) { Write-Host -ForegroundColor Yellow -BackgroundColor Black "The AD group $T0HideGroup was not found in AD. `nTerminating script."; Break }
# Bind the parameter to a friendly variable - result of the dynamic parameter
$TargetOU = $PsBoundParameters[$ParameterName]
# Building domain parameters
$RootDSE = (Get-ADRootDSE)
$Configuration = $RootDSE.configurationNamingContext
$Domain = Get-ADDomain
$DirectoryServiceDN = "CN=Directory Service,CN=Windows NT,CN=Services," + $Configuration
$dSHeuristics = (Get-ADObject -Identity $DirectoryServiceDN -Properties dSHeuristics).dSHeuristics
$T0HideGroupSID = New-Object System.Security.Principal.SecurityIdentifier (Get-ADGroup -Identity $T0HideGroup).sid
$ParentACL = Get-Acl -Path $('AD:\' + $TargetOU)
$UsersContainerACL = Get-Acl -Path $('AD:\CN=Users,' + $domain.DistinguishedName)
$BuiltinContainerACL = Get-Acl -Path $('AD:\CN=Builtin,' + $domain.DistinguishedName)
$AdminSDHolderACL = Get-Acl -Path $('AD:\CN=AdminSDHolder,CN=System,' + $Domain.DistinguishedName)
$ProtectedObjects = (Get-ADObject -Filter { admincount -eq 2 }).DistinguishedName
}
process
{
switch ($Undo)
{
false{
if ($null -ne $dSHeuristics)
{
$LOMStatus = $dSHeuristics[2]
if ($LOMStatus -match 0)
{
$NewdSHeuristics = $($dSHeuristics.Substring(0, 2)) + $($dSHeuristics.Substring(2, 1).replace("0", "1")) + $($dSHeuristics.Substring(3, $($dSHeuristics.Length - 3)))
Set-ADObject -Identity $DirectoryServiceDN -Replace @{ 'dSHeuristics' = $NewdSHeuristics }
}
}
else
{
Set-ADObject -Identity $DirectoryServiceDN -Replace @{ 'dSHeuristics' = '001' }
}
$AdminSDHolderACL.AddAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule $T0HideGroupSID, "ListObject", "Deny"))
Set-Acl -AclObject $AdminSDHolderACL -Path $('AD:\CN=AdminSDHolder,CN=System,' + $Domain.DistinguishedName)
$ParentACL.AddAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule $T0HideGroupSID, "ListChildren", "Deny"))
Set-Acl -AclObject $ParentACL -Path $('AD:\' + $TargetOU)
$UsersContainerACL.AddAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule $T0HideGroupSID, "ListChildren", "Deny"))
Set-Acl -AclObject $UsersContainerACL -Path $('AD:\CN=Users,' + $domain.DistinguishedName)
$BuiltinContainerACL.AddAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule $T0HideGroupSID, "ListChildren", "Deny"))
Set-Acl -AclObject $BuiltinContainerACL -Path $('AD:\CN=Builtin,' + $domain.DistinguishedName)
$ProtectedObjects.foreach{
Set-Acl -AclObject $AdminSDHolderACL -Path $_
}
} true {
if ($null -ne $dSHeuristics)
{
$LOMStatus = $dSHeuristics[2]
if ($LOMStatus -match 0)
{
$NewdSHeuristics = $($dSHeuristics.Substring(0, 2)) + $($dSHeuristics.Substring(2, 1).replace("0", "1")) + $($dSHeuristics.Substring(3, $($dSHeuristics.Length - 3)))
Set-ADObject -Identity $DirectoryServiceDN -Replace @{ 'dSHeuristics' = $NewdSHeuristics }
}
}
else
{
Set-ADObject -Identity $DirectoryServiceDN -Replace @{ 'dSHeuristics' = '001' }
}
$AdminSDHolderACL.RemoveAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule $T0HideGroupSID, "ListObject", "Deny")) | Out-Null
Set-Acl -AclObject $AdminSDHolderACL -Path $('AD:\CN=AdminSDHolder,CN=System,' + $Domain.DistinguishedName)
$ParentACL.RemoveAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule $T0HideGroupSID, "ListChildren", "Deny")) | Out-Null
Set-Acl -AclObject $ParentACL -Path $('AD:\' + $TargetOU)
$UsersContainerACL.RemoveAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule $T0HideGroupSID, "ListChildren", "Deny")) | Out-Null
Set-Acl -AclObject $UsersContainerACL -Path $('AD:\CN=Users,' + $domain.DistinguishedName)
$BuiltinContainerACL.RemoveAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule $T0HideGroupSID, "ListChildren", "Deny")) | Out-Null
Set-Acl -AclObject $BuiltinContainerACL -Path $('AD:\CN=Builtin,' + $domain.DistinguishedName)
$ProtectedObjects.foreach{
Set-Acl -AclObject $AdminSDHolderACL -Path $_
}
}
}
}
end
{
switch ($Undo)
{
false {
Write-Host "T0-Objects will not be seen by members of the group $((Get-ADObject -Identity $T0HideGroup).Name)."
} true {
Write-Host "T0-Objects will be seen by members of the group $((Get-ADObject -Identity $T0HideGroup).Name) and every else authenticated user."
}
}
}
}