Salut les gens,
Aujourd’hui on va s’attarder (un bon moment) sur un mécanisme de sécurité dans le service d’annuaire Active Directory de Microsoft : AdminSDholder.
AdminSDHolder
Le mécanisme dit couramment « AdminSDHolder » est un processus qui s’exécute toutes les heures sur le PDC d’un domaine Active Directory, et qui modifie les ACLs (pour Access Control Lists) des objets de l’AD ayant l’attribut « admincount » à 1. L’ACL est modifiée pour casser l’héritage et appliquer les mêmes droits que ceux en place sur l’objet AdminSDHolder dans le schéma, ici :
CN=AdminSDHolder, CN=System, DC=equestria, DC=com
Comment se retrouve-t-on avec admincount à 1 sur son compte AD ?
Par une contamination de zombie par morsure évidemment, hein ! Plus exactement, au départ dans un Active Directory, certains groupes et utilisateurs « sensibles » (dit, Protected Groups et au nombre de 13 en 2008R2) ont déjà cet attribut à 1 par défaut, (voir KB318180). Par exemple le groupe « Admins du Domaine » ou le compte « krbtgt ». Ensuite tout objet qui devient membre d’un de ces groupes (y compris par héritage) est alors « contaminé » et son attribut admincount passe également à 1.
Pourquoi ce mécanisme ?
Cette tâche planifiée a pour objectif de protéger les objets ayant des privilèges élevés sur le système d’informations, en particulier quand de la délégation de droits est en place sur votre AD.
Prenons un exemple :
Tout d’abord, nous avons l’O.U Utilisateurs, où je place des objets de type ‘user’, j’ai une délégation en place qui permet à certains opérateurs approuvés de faire des actions sur les comptes utilisateurs de cette O.U, par exemple : réinitialiser le mot de passe du compte.
Ensuite, Par erreur ou par mauvaise gestion de mon AD, un de mes comptes utilisateurs présent dans cette O.U devient membre de « Admins du domaine » (ou d’un autre groupe protégé donnant des privilèges élevés).
Avec la délégation est en place, mon opérateur pourra usurper l’identité du compte membre d’« Admins du domaine », en réinitialisant le mot de passe du compte… et finir roi du pétrole sur ce domaine en moins de deux.
- Le mécanisme d’AdminSDHolder est là pour prévenir cet axe d’attaque en remplaçant les ACLs sur ce compte utilisateur par celle du conteneur AdminSDHolder sur lequel il n’y pas de délégation en place, ainsi notre opérateur ne pourra pas réinitialiser le mot de passe de ce compte pour l’usurper, car il n’aura plus d’accès sur ce dernier.
Le problème
La difficulté de gestion autour de ce mécanisme est que lorsqu’on retire à un objet son appartenance à un des groupes sensibles, l’attribut admincount reste à 1, malgré le fait que le compte n’ait plus de privilèges en particulier.
Pourquoi admincount reste-t‘il à 1 ? Tout simplement, car les groupes concernés par AdminSDHolder donnent des privilèges trop élevés pour qu’on puisse simplement « oublier » que le compte les a eu. Avec ses privilèges usurpés ou légitime, le compte aurait très bien pu, par exemple :
- Se créer d’autres comptes Admins dans le domaine ;
- se donner des droits sur l’AD, sur les données ; bref,
- assurer sa mainmise sur le SI pour un attaquant.
Du coup, le système laisse l’attribut tagué à un.
Que faire dans ces cas-là
Ce que dit Microsoft :
Quand on fouille un peu dans les documentations Microsoft, on se rend compte que la préconisation de l’éditeur quand un compte à admincount à 1, mais n’est plus membre d’un des Protected Groups par défaut : c’est de supprimer le compte !
Pourquoi ? Car on considère qu’à partir du moment où un compte à eu ces privilèges il a eu la possibilité de s’en octroyer plus pendant son élévation, et que donc on a perdu la maîtrise de ces accès. En conséquence, il n’y aurait aucun intérêt à remettre le compte avec les ACLs par défaut.
Pourquoi c’est bien, mais pas top non plus pour dans la vraie vie.
Alors c’est bien car ça veut dire que tout compte ayant été membre d’un Protected Group se retrouve tatoué par un admincount à 1. Et ça, c’est toujours rigolo pour se rendre compte que le stagiaire a cet attribut réglé sur son compte… ou pas d’ailleurs…
La difficulté c’est que ce mécanisme n’est pas forcement connus des admins, ni géré par ces derniers. Du coup avec un peu d’historique, on peut se retrouver rapidement avec un paquet de comptes dont les ACLs sont cassées. De même, certains des comptes en questions peuvent être des VIP qui à une autre époque ou la sécurité était moins critique avaient des privilèges fort, qu’ils n’ont plus maintenant (heureusement), mais allez leur expliquer qu’il faut supprimer leurs comptes pour autant…
Que faire ?
La première étape est déjà de prendre conscience de ce mécanisme, ensuite si possible supprimer autant que possible les comptes qui ne sont plus légitimes, et regrouper dans une OU ceux qui restent. Pour ceux qu’on ne peut ni supprimer, ni mettre de côté, il va falloir les gérer.
Comment réinitialiser AdminCount
Comme je l’ai dit la tâche AdminSDHolder casse les ACLs sur les objets concernés. Du coup notre travail n’est pas aussi simple qu’un simple :
Set-ADUser 'Unikitty' -Clear 'AdminCount'
Pourquoi ? Tout simplement parce que les ACLs sur l’objet ne serait pas réinitialisée ! Et qu’en plus ça ne sert à rien si l’on ne vérifie pas que l’objet n’est pas toujours membre d’un ou plusieurs Protected Groups avant de réaffecter l’attribut…
En gros l’algorithme qu’on voudrait c’est :
$Liste = Get-Adobjet * Where-Object {$_.AdminCount -eq 1} Foreach($obj in $liste){ if($obj estMembreRecursif $protectedGroups){ Move-ADObject -identity $obj -Dest 'OU=Admins[…]' }else{ Set-ADUser $obj -Clear 'AdminCount' Reset-ADObjectACL $obj } }
Implémentation
Le premier truc dont on a besoin c’est la liste des Protected Groups par défaut, comme je l’ai dit c’est référencé dans le KB318180. La bonne nouvelle, c’est que ce sont tous les objets sont des « Well-Known-SIDs » (listés dans le KB243330), donc on n’aura pas à s’embêter avec les problèmes de langue dans l’AD (exemple : “Domain Admins” versus “Admins du domaine” en français). Seule difficulté, certains de ces SID dépendent de celui du domaine ou du domaine racine de forêt qu’on va donc devoir récupérer.
SID du domaine et du domaine racine la fôret
Je passe sur la construction d’un SID d’objet, il y a plein de docs disponibles sur le net pour ça. Voici comment on récupère celui du domaine racine d’une forêt :
<# .SYNOPSIS Récupère l’objet AD Domain de la racine pour la forêt courante. .DESCRIPTION Remonte l’arborescence de domaine jusqu’à la racine et renvoi l’objet .PARAMETERS domain Le domaine de départ, par défaut le courant .OUTPUTS l’objet AD Domaine racine de la forêt .EXAMPLE PS C:> Get-ADForestRootDomain -domain unico.rn .AUTHORS TiTi .LASTUPDATE 2015-06-08 #> Function Get-ADForestRootDomain { [CmdletBinding()] Param ( $domain = (Get-ADDomain) ) $current = Get-ADDomain $domain #on remonte dans l’arborescence, c’est-à-dire : tant que le domaine parent n’est pas vide. while($current.ParentDomain -ne $null){ $current = Get-ADDomain $current.ParentDomain } Return $current }
Qu’on appelle ensuite en demandant le SID
$RootDomainSid = [String] (Get-ADForestRootDomain).DomainSID
Pour le domaine en cours c’est encore plus simple :
$CurrentDomainSid = [String] (Get-ADDomain).DomainSID
Groupes protégés
Du coup la liste de nos groupes devient simple à construire
$DefaultProtectedGroupSids = @('S-1-5-32-548','S-1-5-32-544','S-1-5-32-551',"$CurrentDomainSid-512","$CurrentDomainSid-516","$RootDomainSid-519",'S-1-5-32-550',"$CurrentDomainSid-498",'S-1-5-32-552',"$RootDomainSid-518",'S-1-5-32-549') #+2 comptes 'S-1-5-32-500=Administrator' and $CurrentDomainSid-502 # Soit au total : @('Account Operators','Administrator','Administrators','Backup Operators','Domain Admins','Domain Controllers','Enterprise Admins','Krbtgt','Print Operators','Read-only Domain Controllers','Replicator','Schema Admins','Server Operators')
Bon voilà, cette partie c’était fastoche…
Note : au passage un collègue anonyme (toujours le même que là) m’a appris qu’on pouvait aussi utiliser l’objet .NET suivant : [System.Security.Principal.WellKnownSidType], qui permet d’éviter de se taper la construction des SIDs. Trop tard pour ici, mais je vous invite à y jeter un œil…
Construction des ensembles d’objets AD
Récupération des objets ayant AdminCount à 1
Là aussi rien de bien compliqué, un bon Get-ADObject -filter fait le boulot.
Get-ADObject -Filter {(admincount -eq 1)} -Properties @('admincount','memberof')
Normalement il devrait y avoir que des groupes et utilisateurs dans la liste, vous pouvez vérifier en filtrant votre résultat :
Get-ADObject -Filter {(admincount -eq 1)} -Properties @('admincount','memberof') | Where-Object {$_.ObjectClass -ne 'user' -and $_.ObjectClass -ne 'group'}
De mon côté j’ai préféré séparer les objets groupes et utilisateurs dans 2 listes différentes. Car ensuite je n’ai pas appliqué le même traitement pour détecter si un objet est bien membre d’un groupe protégé. Du coup, ça me donne deux ensembles :
$AdminCountUserList et $AdminCountGroupList
Récupération des membres légitimes des groupes protégés.
Ces objets sont membres des groupes protégés, il est normal qu’ils aient admincount à 1. Pour lister ces Objets, j’ai utilisé deux techniques différentes (parce que je peux) pour avoir les membership récursifs de mes objets :
- Une pour les utilisateurs, je me suis servi de l’opérateur -RecursiveMatch de l’option filter dans Get-ADUser (expliquer très bien cet article):
$ProtectedGroupActualUserMember = @() $ProtectedGroupActualUserMember = foreach($group in $DefaultProtectedGroupSids){Get-ADUser -Filter ("memberOf -RecursiveMatch"+"'"+((Get-ADGroup $group).DistinguishedName -replace "'","''") +"'") -SearchBase (Get-ADDomain).DistinguishedName} $ProtectedGroupActualUserMember = $ProtectedGroupActualUserMember | Select-Object -Unique
- Une pour les groupes, Celle-ci est basée sur une fonction récursive maison (le récursif : c’est bon mangez-en), que voici :
<# .SYNOPSIS Fournie la liste des groupes imbriqués pour un groupe donné .DESCRIPTION Appel récursif sur l’attribut member du groupe en entrée pour obtenir la liste des membres de types « group » du groupe en entrée, tout en gérant les boucles éventuelles dans les groupes enfant. .PARAMETERS Group Le groupe à traiter .PARAMETERS ProcessedGroup Les groupes déjà traités pour éviter les boucles imbriquée, et appels infinis. .OUTPUTS La liste des sous-groupes du groupe en entrée (incluant le groupe initial). .EXAMPLE PS C:> Get-ADGroupSubGroupMember 'test3' .AUTHORS TiTi .LASTUPDATE 2015-06-11 #> Function Get-ADGroupSubGroupMember { [CmdletBinding()] Param( $Group, $ProcessedGroup = @() ) #groupe traité $ProcessedGroup += Get-ADGroup $Group #On récupère les sous-groupes $SubGroupMember = @(Get-ADGroupMember $Group | Where-Object {$_.objectClass -eq 'group'}) if($SubGroupMember -eq $null){ #si pas de sous-groupe, on retourne les groupes traités return $ProcessedGroup }else{ #Pour chaque sous-groupe(s), on rappelle la fonction foreach($SubGroup in $SubGroupMember){ if($SubGroup.SID -notin $ProcessedGroup.SID){ $ProcessedGroup = @(Get-ADGroupSubGroupMember -Group $SubGroup -ProcessedGroup $ProcessedGroup) } } # Et on retourne les groupes traités return $ProcessedGroup } }
On appelle ensuite cette fonction normalement pour chaque groupe protégé du KB, de manière à connaître tous les groupes propageant légitimement l’attribut admincount à 1.
$ProtectedGroupActualUserMember = @() foreach($group in $DefaultProtectedGroupSids){ $ProtectedGroupActualUserMember += @(Get-ADGroupSubGroupMember $group) } $ProtectedGroupActualUserMember = $ProtectedGroupActualUserMember | Select-Object -Unique
Intersection des 4 ensembles d’objets, reste et traitement.
À ce moment de l’article, on a donc quatre variables qui représentent les quatre ensembles suivant :
- $Administrationaliser : Tous les objets de type « compte utilisateur » ayant l’attribut admincount positionné à 1
- $AdminCountGroupList : Tous les objets de type « groupe » ayant l’attribut admincount positionné à 1
- $ProtectedGroupEspectedGroupMember : Les groupes effectivement membres d’un protectedgroup, y compris par transitivité. Et ayant donc AdminCount à 1 légitimement.
- $ProtectedGroupEspectedUserMember : Les utilisateurs effectivement membres d’un protectedgroup, y compris par transitivité. Et ayant donc AdminCount à 1 légitimement.
Il nous suffit de croiser la liste complète avec celle effectivement attendue. Via une simple double boucle imbriquée :
$UserWithUnwantedACL = @() $UserWithAdminCountOk = @() #Pour chaque objet user/group ayant AdminCount à « 1 » foreach($user in $AdminCountUserList){ $i=0 #On cherche s’il appartient à un des Protected Group « étendus » while(($i -lt $ProtectedGroupActualUserMember.count) -and ($ProtectedGroupActualUserMember[$i].SID -ne $user.SID)){ $i++ } if($i -eq $ProtectedGroupActualUserMember.count){ $UserWithUnwantedACL += @($user) # si non, on l’ajoute à ce tableau-ci }else{ $UserWithAdminCountOk += @($user) # si oui, on l’ajoute à ce tableau-là } }
La boucle par les groupes est exactement la même, il suffit de remplacer user par groupe dans le script à peu de chose près. Sinon dans une seule boucle en jouant avec des Where-Object {$_.ObjectClass -eq ‘<class>’}
Traitement des objets
À ce moment, on a encore quatre listes avec 2 types d’objets :
- Les objets avec admincount à 1, mais qui ne sont pas membre d’un protectedGroup
- Les objets avec admincount à 1, membre d’un protectedGroup
Pour les objets « A » on veut remettre admincount à « not-set » et reclaquer l’ACL par défaut.
AdminCount à zéro
Pour « reseter » l’attribut, rien de plus simple :
Set-ADUser $user -Clear AdminCount
Reset-ACL
La partie ACL est plus compliquée, j’avais commencé à regarder du côté du .NET et des classes :
System.DirectoryServices.ActiveDirectoryRights ;
System.Security.AccessControl.AccessControlType ;
System.DirectoryServices.ActiveDirectorySecurityInheritance ; ou encore
System.DirectoryServices.ActiveDirectoryAccessRule.
Il s’avère qu’elles sont parfaites pour appliquer des ACLs spécifiques sur des objets AD, beaucoup moins évidentes à utiliser pour réappliquer celle par défaut sur un objet. Par contre en cherchant un peu, j’ai trouvé l’utilitaire « dsacl » (pour Directory Services ACL) qui fait exactement ça pour nous.
Un coup de RTFM plus tard on trouve que :
dsacls.exe "$($user).DistinguishedName" /S
fait exactement ce qu’on veut. Soit, dans un beau Invoke-Expression :
Invoke-Expression "dsacls.exe '$($user).DistinguishedName' /S"
Ce qui nous donne un résultat à implémenter :
foreach($obj in @($UserWithUnwantedACL+$GroupWithUnwantedACL)){ Set-ADObject $obj -Clear AdminCount Invoke-Expression "dsacls.exe '$($obj).DistinguishedName' /S" }
Déplacement des objets restants dans une O.U. donnée
Pour les objets « B », on souhaite simplement les déplacer dans une O.U spécifique (sauf si c’est un Protected Group, auquel cas on n’y touche pas). Du coup, Ce n’est pas très compliqué non plus, à l’aide d’un simple Move-ADObject
foreach($obj in @($UserWithAdminCountOk+$GroupWithAdminCountOk)){ if($obj.SID -notin $DefaultProtectedGroupSids){ Move-ADObject -Identity "$obj" -TargetPath "OU=Moved,OU=Administration,DC=Equestria,DC=COM" } }
Et voilà. Désormais il ne reste plus qu’à surveiller nos objets ayant AdminCount à 1, s’assurer que les nouveaux aillent bien là où l’on veut. Et investiguer sur ceux qui pourraient apparaître à l’avenir.
Conclusion
Voilà, j’espère que vous avez tout compris. Je vous le laisse en exercice de reconstruire le script complet, mais en copiant collant les différents morceaux ça devrait pas être trop compliqué non plus…
Il y a pas mal de chose à retenir de ce script :
- Construction de Well-Known-SIDs ;
- Recherche d’objets AD ayant une valeur d’attribut remarquable ;
- Compréhension du mécanisme d’AdminSDHolder ;
- Fonction récursive, et recherche d’appartenance à un groupe pour un objet, et liste des sous-groupes pour un groupe ;
- Manipulation d’objets AD (déplacement, attributs) ;
- Réinitialiser une ACL ;
En bonus et pour finir je vous donne la commande récursive de DSACL pour réinitialiser les ACLS par celles par Défaut sur tout votre AD (si vous avez été très, très mauvais) :
dsacls.exe "DC=Equestria, DC=com" /S /T #T pour « Tree », i.e l'arborescence.
@++
Get-ADObject -Filter {(admincount -eq 1)} -Properties @(‘admincount’,’memberof’) | Where-Object {$_.ObjectClass -ne ‘user’ -and $_.ObjectClass -ne ‘group’}
c est pas plutot un -or ?
merci en tout cas!
Hello max,
Pour le coup, ça fait 5 ans que j’ai écrit l’article, j’avoue que je ne sais plus trop là :-s.
Il faudra que je recreuse le sujet…Mais en réfléchissant vite fait à la logique :
{$_.ObjectClass -ne ‘user’ -and $_.ObjectClass -ne ‘group’} = ni un objet user ni un objet group, donc tout sauf ces 2 classes.
Alors que :
{$_.ObjectClass -ne ‘user’ -or $_.ObjectClass -ne ‘group’} = pas un objet user ou bien pas un objet group, du coup ça risque de matcher tout puisque les objets ne sont que d’une seul classe. (Lazy evaluation : un group n’est pas un user : vrai donc on match pas ce qu’on veut là)
Donc je dirai que je suis bon… prove me wrong ?
@+