Remédiation Active Directory avec Ping Castle
Toutes mes notes sur les remédiations à faire pour chaque vulnérabilité trouvée par Ping Castle
Active Directory est globalement un livre ouvert : n’importe quel utilisateur peut lire la plupart des informations de l’annuaire (qui est membre de quoi, quel est le numéro de téléphone de John, qui est le manager de Jane…). Quelques attributs font exception (comme unicodePwd qui contient le mot de passe de l’objet), mais ceux-ci sont gérés par les contrôleurs de domaine et ne peuvent pas être utilisés à nos propres fins.
Comment stocker alors dans l’AD des informations confidentielles qui ne soient pas visibles par tous ? Windows LAPS et les mots de passe des comptes gMSA montrent qu’il est possible de protéger et de contrôler l’accès à ce type de données, en utilisant des mécanismes de chiffrement et de visibilité des attributs. On va donc s’en inspirer pour concevoir des solutions adaptées à nos propres besoins.
Si vous avez un Active Directory avec un modèle de tiering déployé et que vous devez utiliser un mot de passe différent pour chaque compte, les smart cards sont une solution merveilleuse pour vous simplifier la vie. Grâce à elles, vous pouvez vous connecter avec l’ensemble de vos comptes sans avoir à taper de mot de passe : un code PIN suffit !
Pour résumer, une smart card c’est :
…et si le code PIN est personnel (c’est le “P” dans “PIN”), le code PUK est utile à votre administrateur pour éviter d’avoir à refaire tous vos certificats si jamais vous avez oublié votre PIN (ou que vous avez bloqué votre clé après plusieurs mauvais codes PIN).
Vient alors la réflexion du stockage de ces codes PUK, et pour ça, plusieurs options sont possibles :
Toutes ces options ont un inconvénient majeur : elles ne suivent pas le cycle de vie des utilisateurs Active Directory. Au fil des départs et des arrivées, des changements de nom ou d’équipe, vous allez finir par vous y perdre. Impossible d’identifier rapidement les utilisateurs qui n’ont pas de code PUK, ou ceux qui ont quitté l’entreprise depuis déjà plusieurs années.

L’idée de cet article est donc de proposer autre chose : une méthode purement expérimentale qui va lier directement l’utilisateur à sa smart card dans Active Directory ; sans pour autant que le code PUK soit accessible à tous.
Le principe s’inspire très largement du travail que Microsoft a déjà effectué pour la gestion des clés BitLocker sur les comptes ordinateurs, ainsi que des mots de passe LAPS et de ceux des comptes gMSA. On va créer une nouvelle classe d’objet représentant nos smart cards dans notre schéma Active Directory (vous comprenez maintenant le côté expérimental). Ces nouveaux objets “smart cards” viendraient se greffer comme des objets enfants des comptes utilisateurs existants, pour se lier durablement à eux sans être impactés par un déplacement ou un renommage de l’objet parent.
Pour accompagner cette nouvelle classe, on vient également ajouter trois nouveaux attributs :
Les codes PUK ne doivent évidemment être accessibles qu’au personnel autorisé. Pour ça, on part sur une approche ceinture-bretelles avec :
Petit rappel, mais modifier son schéma sans savoir ce que l’on fait n’est clairement pas une bonne idée. Ne faites évidemment pas ça sur votre production. Chaque changement fait à votre schéma est indélébile et peut impacter la stabilité de votre domaine Active Directory.
Pour pouvoir modifier le schéma, créer notre classe et nos nouveaux attributs, il va falloir :
On va avoir besoin de trois nouveaux attributs dans le schéma :
corp-pukcorp-pukHistorycorp-pukLastSetLes attributs sont préfixés par “CORP” pour éviter une éventuelle collision avec de nouveaux attributs qui pourraient être ajoutés dans le schéma par Microsoft ou un éditeur tiers.
Voici les spécifications techniques pour chacun :
corp-puk va être une chaîne d’octet confidentielle :
| Attribut | Valeur |
|---|---|
| name | corp-puk |
| objectClass | attributeSchema |
| adminDescription | Contains the current personal unlocking key (PUK) |
| attributeSyntax | 2.5.5.10 (Octet String) |
| isSingleValued | True |
| omSyntax | 4 (Octet String) |
| searchFlags | 128 (Confidential) |
Tout comme corp-puk, corp-pukHistory sera une chaîne d’octets confidentielle, mais multivaluée :
| Attribut | Valeur |
|---|---|
| name | corp-puk |
| objectClass | attributeSchema |
| adminDescription | Contains all the old PUK values |
| attributeSyntax | 2.5.5.10 (Octet String) |
| isSingleValued | False |
| omSyntax | 4 (Octet String) |
| searchFlags | 128 (Confidential) |
Ici, pas besoin de déclarer l’attribut comme confidentiel, on définit simplement le bon format pour avoir une belle date bien propre (comme l’attribut accountExpirationDate par exemple) :
| Attribut | Valeur |
|---|---|
| name | corp-puk |
| objectClass | attributeSchema |
| adminDescription | Contains the last time the PUK was set |
| attributeSyntax | 2.5.5.11 (DateTime) |
| isSingleValued | True |
| omSyntax | 24 (DateTime) |
| searchFlags | 0 (Not indexed) |
Vous pouvez retrouver le tableau officiel de Microsoft pour obtenir les valeurs attendues pour
attributeSyntaxetomSyntaxici : LDAP Representations | Microsoft Learn.
Voici la commande pour l’attribut corp-puk, que vous pouvez recycler en mettant à jour les différentes valeurs indiquées dans les spécifications :
$name = 'corp-puk'
$splat = @{
Name = $name
Type = 'attributeSchema'
OtherAttributes = @{
AdminDescription = 'Contains the current personal unlocking key (PUK)'
AdminDisplayName = $name
AttributeSyntax = '2.5.5.10'
IsSingleValued = $true
LDAPDisplayName = $name
OMSyntax = 4
SearchFlags = 128
AttributeID = (New-Oid)
}
Path = (Get-ADRootDSE).schemaNamingContext
}
New-ADObject @splat
N’oubliez pas qu’une seule erreur sera visible à jamais dans votre schéma.
Il faut maintenant passer aux spécifications de la classe qui va héberger nos nouveaux attributs :
| Attribut | Valeur |
|---|---|
| name | corp-smartCard |
| objectClass | classSchema |
| adminDescription | Device that can be used to authenticate |
| defaultHidingValue | True |
| mayContain | serialNumber, corp-pukHistory, corp-pukLastSet |
| mustContain | corp-puk |
| objectClassCategory | 1 (STRUCTURAL) |
| possSuperior | user, inetOrgPerson |
| showInAdvancedViewOnly | True |
| systemOnly | False |
| subClassOf | 2.5.6.0 (top) |
Voici la dernière étape pour la modification du schéma : la création de notre nouvelle classe d’objet corp-smartCard qui va utiliser les attributs créés précédemment.
$name = 'corp-smartCard'
$schemaPath = (Get-ADRootDSE).schemaNamingContext
$mustContain = Get-ADObject -Filter { Name -eq 'corp-puk' } -SearchBase $schemaPath -Properties AttributeID
$mayContain = 'serialNumber', 'corp-pukHistory', 'corp-pukLastSet' | ForEach-Object {
Get-ADObject -Filter { Name -eq $_ } -SearchBase $schemaPath -Properties AttributeID
}
$splat = @{
Name = $name
Type = 'classSchema'
OtherAttributes = @{
AdminDisplayName = $name
AdminDescription = 'Device that can be used to authenticate'
DefaultHidingValue = $true
GovernsID = (New-OID)
LDAPDisplayName = $name
MayContain = $mayContain.AttributeID
MustContain = $mustContain.AttributeID
ObjectClassCategory = 1
PossSuperiors = 'user', 'inetOrgPerson'
ShowInAdvancedViewOnly = $true
SystemOnly = $false
SubClassOf = '2.5.6.0'
}
Path = (Get-ADRootDSE).schemaNamingContext
}
New-ADObject @splat
Ça n’aura pas été aussi facile que prévu puisqu’il n’y a que très peu de documentation sur le sujet, mais sachez que les attributs “MayContain” et “MustContain” ne reçoivent que les OID des attributs cibles (si jamais ça peut vous aider).
Je l’ai caché dans mon module, et celui-ci devrait être plus visible mais les codes PUK ne sont déchiffrables que par les membres du groupe “SmartCard PUK Administrators”. Il convient donc de créer ce groupe dans Active Directory :
New-ADGroup -Name 'SmartCard PUK Administrators' -GroupScope Global -GroupCategory Security
Si vous voulez pouvoir déchiffrer les codes PUK, vous aurez besoin d’être membre du groupe.
Vous vous souvenez de l’explication sur les attributs qui ne pouvaient être lus que par les comptes autorisés ? Eh bien, comme le groupe “Domain Admins” possède déjà ces permissions, je m’en accommode très bien. Si vous souhaitez mettre en place une délégation, voici ce que Claude m’a proposé (je n’ai pas testé) :
$path = "OU=Utilisateurs,DC=corp,DC=contoso,DC="
$group = "CORP\SmartCard PUK Administrators"
$attr = "corp-puk"
# 1. Lecture de l'attribut
dsacls $path /I:S /G "${group}:RP;$attr;corp-smartCard"
# 2. Control Access (requis pour les attributs confidentiels)
dsacls $path /I:S /G "${group}:CA;;corp-smartCard"
Avant de mettre en place cette délégation, souvenez-vous qu’une seule smart card peut permettre d’utiliser plusieurs comptes, donc n’allez pas faire une délégation qui pourrait ouvrir un nouveau chemin d’attaque vers les membres du groupe “Domain Admins”.
Une fois notre schéma cassé modifié, on peut passer à l’exploitation de nos nouveaux jouets ! Pour ça, j’ai créé un module PowerShell assez simple qui permet d’interagir avec nos smart cards :
Celui-ci est composé de quelques commandes utiles :
New-ADSmartCard pour créer une nouvelle smart cardGet-ADSmartCard pour lister les smart cards et obtenir leur code PUKSet-ADSmartCard pour modifier une smart cardRemove-ADSmartCard pour supprimer évidemmentCréation d’une nouvelle smart card pour l’utilisateur “John Smith”, sans jamais saisir le code PUK en clair dans la console :
New-ADSmartCard -Name '28293929' -User (Get-ADUser john.smith)
Comme le code PUK est obligatoire, il sera automatiquement récupéré via un prompt supplémentaire au format SecureString.
Ici j’ai indiqué le numéro de série (SerialNumber) de la smart card dans la valeur du paramètre “Name”. A mon humble avis, c’est le meilleur choix à faire.
Obtenir toutes les smart cards du domaine :
Get-ADSmartCard -Filter *
Obtenir toutes les smart cards d’un utilisateur :
Get-ADUser john.smith | Get-ADSmartCard
Obtenir le code PUK en clair de la smart card “28293929” :
Get-ADSmartCard 28293929 -AsPlainText
Pour mettre à jour le code PUK de la smart card “28293929” :
Get-ADSmartCard 28293929 |
Set-ADSmartCard -PUK (Read-Host -AsSecureString -Prompt 'New PUK')
Et pour finir, la commande Remove-ADSmartCard :
Get-ADSmartCard 28293929 | Remove-ADSmartCard
Bon le POC est rigolo, mais une modification de schéma ne doit évidemment pas être prise à la légère. Dans 99% des cas, ça vous sera probablement refusé purement et simplement. Du coup voici une alternative pour stocker des informations confidentielles sans pour autant faire péter le rôle “Schema Admins”.
On va essayer d’identifier un attribut inutilisé de votre domaine. Pour ça, on peut utiliser la commande PowerShell la plus sous-cotée (Group-Object). Voici comment vérifier l’utilisation de l’attribut msds-CloudExtensionAttribute1 sur l’ensemble de vos utilisateurs :
$attribute = 'msds-CloudExtensionAttribute1'
Get-ADUser -Filter * -Properties $attribute | Group-Object $attribute
Les attributs msds-CloudExtensionAttribute... sont des candidats idéaux, et ils existent depuis 2012. Seul problème : ils ne sont pas du type “Octet String” mais simplement “String”. On va donc devoir convertir notre chaîne de bits chiffrés en une chaîne de caractères simple.
Contrairement à notre plan “parfait” avec modification du schéma, ici on va devoir convertir notre Octet String en une chaîne de caractères. La bonne nouvelle, c’est que le Base64 permet de faire ça facilement :
[Convert]::ToBase64String($blob)
On va donc utiliser notre commande personnalisée issue du module pour obtenir notre chaîne de caractères au format Base64 :
$splat = @{
InputObject = ('test' | ConvertTo-SecureString -AsPlainText -Force)
ToBase64 = $true
Owner = 'Domain Admins'
}
$base64 = ConvertTo-DpapiNg @splat
Une fois l’information chiffrée, il ne reste plus qu’à l’ajouter sur l’attribut msds-CloudExtensionAttribute1 de notre utilisateur :
Set-ADUser john.smith -Replace @{ 'msds-CloudExtensionAttribute1' = $base64 }
Quid de la longueur maximale de la chaîne de caractères ?
D’après mes tests sur les attributs
msds-CloudExtensionAttribute..., on peut y stocker jusqu’à environ un million de caractères. Attention quand même, je ne pense pas qu’ajouter 1 MB de données par utilisateur soit une bonne idée sur le long terme !
Enfin, pour déchiffrer la donnée, il faut faire le chemin inverse en faisant la conversion de Base64 vers Octet String :
$user = Get-ADUser john.smith -Properties 'msds-CloudExtensionAttribute1'
$blob = [Convert]::FromBase64String($user.'msds-CloudExtensionAttribute1')
ConvertFrom-DpapiNg -InputObject $blob
Avec la combinaison de DPAPI-NG et du drapeau “CONFIDENTIEL” sur les attributs dans le schéma, il est possible de cacher des informations dans Active Directory et de bénéficier ainsi du cycle de vie naturel de vos objets. La création d’une classe dédiée offre une solution intégrée, mais reste expérimentale et implique une modification irréversible du schéma. Pour un besoin plus simple, réutiliser un attribut existant peut éviter cette étape, après avoir vérifié qu’il est libre et adapté, évidemment !
Toutes mes notes sur les remédiations à faire pour chaque vulnérabilité trouvée par Ping Castle