Aller au contenu

Accueil / Sécurité et identité

NTLM: lire un échange NTLMSSP dans Wireshark

Décoder NEGOTIATE, CHALLENGE et AUTHENTICATE dans Wireshark, vérifier NTLMv2 et le MIC, recalculer la NTProofStr et voir ce qu'une capture livre à un attaquant.

Rémi Vernier
7 novembre 2026 · 8 min de lecture

Quand un client Windows s’authentifie en NTLM sur un serveur, que voit-on réellement passer sur le réseau, et comment être certain qu’il s’agit bien de NTLMv2 avec un MIC et pas d’un vieux NTLMv1? La réponse tient en trois messages NTLMSSP (NEGOTIATE, CHALLENGE, AUTHENTICATE) que Wireshark décode nativement avec le filtre ntlmssp. La longueur de la réponse du client, la présence d’une NTProofStr et la valeur de l’attribut MsvAvFlags suffisent pour trancher.

Dans cet article nous allons capturer une authentification NTLM sur une maquette, lire chaque message champ par champ, puis recalculer la NTProofStr à partir du mot de passe pour vérifier que nous avons bien compris le mécanisme. Nous verrons ensuite ce que cette même capture apporte à un attaquant.

Présentation de la maquette

Nom du domaine: test.local (TESTLOCAL)

Contrôleur de domaine: dc01.test.local

Serveur web: srv01.test.local (Windows Server 2022, IIS 10), site monsite.test.local

Client: WIN11, compte user1@test.local, mot de passe Azerty123

Pour être certain de ne voir que du NTLM, nous supprimons le fournisseur « Negotiate » dans les paramètres de l’authentification Windows du site et ne laissons que « NTLM ». Le serveur répondra alors uniquement WWW-Authenticate: NTLM et le client n’essaiera même pas Kerberos. Si votre objectif est au contraire de comprendre pourquoi un site configuré en Negotiate retombe en NTLM, nous avions déroulé ce diagnostic dans l’article Pourquoi Kerberos retombe en NTLM.

Attention: le site est publié en HTTP sur le port 80 pour que Wireshark lise les en-têtes sans déchiffrement TLS. C’est acceptable sur une maquette isolée, jamais en production. En HTTPS, il faudra exporter les clés de session via la variable d’environnement SSLKEYLOGFILE du navigateur et les déclarer dans les préférences TLS de Wireshark.

WIN11 (client) srv01 (IIS) dc01 (Netlogon) 1. GET / (anonyme) 2. 401, WWW-Authenticate: NTLM 3. Authorization: NTLM TlRMTVNTUAAB… NEGOTIATE (drapeaux) 4. 401, NTLM TlRMTVNTUAAC… CHALLENGE (nonce + TargetInfo) 5. Authorization: NTLM TlRMTVNTUAAD… AUTHENTICATE (NTLMv2 + MIC) 6. NetrLogonSamLogonEx 7. validation + clé de session 8. 200 OK
Échange NTLM sur HTTP et validation pass-through vers le contrôleur de domaine

Le serveur IIS transmet le challenge et la réponse de user1 au contrôleur de domaine via Netlogon, et c’est le DC, seul détenteur du hash du mot de passe, qui valide l’authentification et renvoie la clé de session. Ce point explique pourquoi une authentification NTLM réussie ne laisse aucune trace Kerberos sur le DC, seulement des événements de validation d’identifiants.

Capture et filtre ntlmssp

Nous lançons Wireshark sur WIN11, sur l’interface réseau du domaine, avec un filtre de capture host srv01.test.local and tcp port 80 pour ne pas se noyer. Nous ouvrons ensuite http://monsite.test.local dans le navigateur, puis nous arrêtons la capture une fois la page affichée.

Le dissecteur NTLMSSP de Wireshark extrait automatiquement le contenu base64 des en-têtes Authorization et WWW-Authenticate, comme il le fait pour SMB, LDAP ou DCE/RPC. La liste complète des champs exploitables est publiée dans la référence des filtres d’affichage ntlmssp. Les filtres utiles:

code
ntlmssp                                   -> les trois messages
ntlmssp.messagetype == 0x00000001         -> NEGOTIATE
ntlmssp.messagetype == 0x00000002         -> CHALLENGE
ntlmssp.messagetype == 0x00000003         -> AUTHENTICATE
ntlmssp.messagetype == 0x00000003 && !ntlmssp.ntlmv2_response   -> AUTHENTICATE sans NTLMv2

Le dernier filtre sert d’audit rapide sur une capture plus large: tout ce qu’il remonte est du NTLMv1 (ou de l’anonyme) et mérite une enquête.

Comme nous l’avions vu dans l’article sur les entêtes HTTP WWW-Authenticate et Authorization, le préfixe TlRMTVNTUA correspond à « NTLMSSP » encodé en base64. Le caractère qui suit trahit le type de message: TlRMTVNTUAAB, …AAC, …AAD pour les types 1, 2 et 3.

Lecture des trois messages

NEGOTIATE

Le client annonce ce qu’il sait faire. L’en-tête envoyé ressemble à ceci:

code
Authorization: NTLM TlRMTVNTUAABAAAAl4II4gAAAAAAAAAAAAAAAAAAAAAKAPRlAAAADw==

Une fois décodé par Wireshark:

code
NTLM Secure Service Provider
    NTLMSSP identifier: NTLMSSP
    NTLM Message Type: NTLMSSP_NEGOTIATE (0x00000001)
    Negotiate Flags: 0xe2088297
        1... = Negotiate 56
        .1.. = Negotiate Key Exchange
        ..1. = Negotiate 128
        .... ..1. = Negotiate Version
        .... 1... = Negotiate Extended Session Security
        1... = Negotiate Always Sign
        .1.. = Negotiate NTLM key
        1... = Negotiate Lan Manager Key
        ...1 .... = Negotiate Sign
        .... .1.. = Request Target
        .... ..1. = Negotiate OEM
        .... ...1 = Negotiate UNICODE
    Version 10.0 (Build 26100); NTLM Current Revision 15

Les drapeaux sont définis dans la section 2.2.2.5 de [MS-NLMP]. Celui qui nous intéresse ici est NTLMSSP_NEGOTIATE_SIGN (0x00000010): le client demande la négociation d’une clé de session pour signer les messages. Selon la spécification, si le client l’envoie, le serveur doit le renvoyer dans le CHALLENGE.

Attention: aucun drapeau n’indique NTLMv1 ou NTLMv2. La spécification le précise dans la section consacrée à NTLM v2: la version est configurée de chaque côté, sous Windows par la stratégie « Sécurité réseau: niveau d’authentification LAN Manager ». Le drapeau « Negotiate NTLM key » existe dans les deux cas, il ne prouve rien. Seul le message AUTHENTICATE permet de trancher.

CHALLENGE

code
NTLM Message Type: NTLMSSP_CHALLENGE (0x00000002)
Target Name: TESTLOCAL
Negotiate Flags: 0xe2898215
NTLM Server Challenge: 3f1a9c62d84b07e5
Target Info
    Attribute: NetBIOS domain name: TESTLOCAL
    Attribute: NetBIOS computer name: SRV01
    Attribute: DNS domain name: test.local
    Attribute: DNS computer name: srv01.test.local
    Attribute: DNS tree name: test.local
    Attribute: Timestamp
    Attribute: End of list
Version 10.0 (Build 20348); NTLM Current Revision 15

Le serveur renvoie les drapeaux retenus (Negotiate Sign est bien présent, Negotiate Target Info s’est ajouté), le nonce de 8 octets 3f1a9c62d84b07e5 et une structure TargetInfo composée d’AV_PAIR. Cette TargetInfo est importante pour la suite: le client va la recopier, enrichie, dans sa réponse NTLMv2, et elle sera couverte par le calcul de la NTProofStr.

AUTHENTICATE

code
NTLM Message Type: NTLMSSP_AUTH (0x00000003)
Lan Manager Response: 000000000000000000000000000000000000000000000000
NTLM Response: a1c406a268ef03d09905c99198bd4748010100000000000000709e0b…
    Length: 242
    NTLMv2 Response
        NTProofStr: a1c406a268ef03d09905c99198bd4748
        Response Version: 1
        Hi Response Version: 1
        Time: Nov  7, 2026 09:12:00 UTC
        NTLMv2 Client Challenge: a41c7e09d25b3f86
        Attribute: NetBIOS domain name: TESTLOCAL
        …
        Attribute: Flags
            Flags: 0x00000002
        Attribute: Target Name: HTTP/monsite.test.local
        Attribute: End of list
Domain name: TESTLOCAL
User name: user1
Host name: WIN11
Negotiate Flags: 0xe2888215
Version 10.0 (Build 26100); NTLM Current Revision 15
MIC: 4e7d…

Trois éléments permettent de répondre à la question de départ:

  1. La NtChallengeResponse fait 242 octets. Une réponse NTLMv1 fait toujours 24 octets, et c’est ce seuil (longueur supérieure à 0x18) que la spécification utilise côté serveur pour reconnaître le NTLMv2. Wireshark l’affiche alors sous « NTLMv2 Response » avec ses sous-champs.
  2. Les 16 premiers octets constituent la NTProofStr, le reste est le « blob » du client: version, horodatage, challenge client de 8 octets et la TargetInfo recopiée.
  3. Dans ce blob, l’attribut MsvAvFlags vaut 0x00000002. D’après la section 3.1.5.2.1 de [MS-NLMP], ce bit indique que le client a fourni un MIC dans le message.

Le MIC est un HMAC_MD5 calculé avec l’ExportedSessionKey sur la concaténation des trois messages NEGOTIATE, CHALLENGE et AUTHENTICATE (champ MIC mis à zéro pendant le calcul). Le serveur le recalcule et, s’il diffère, l’authentification échoue, comme le décrit la section 3.2.5.1.2. Un intermédiaire ne peut donc plus retirer le drapeau Negotiate Sign au passage sans casser le MIC, et il ne peut pas non plus supprimer le MIC puisque le bit 0x2 de MsvAvFlags est lui-même protégé par la NTProofStr.

Recalculer la NTProofStr?

Pour vérifier que nous lisons correctement la capture, rien ne vaut un recalcul. Le pseudo-code de [MS-NLMP] 3.3.2 se résume à trois lignes:

code
NTHash     = MD4(UTF-16LE(motdepasse))
NTOWFv2    = HMAC_MD5(NTHash, UTF-16LE(MAJUSCULE(utilisateur) + domaine))
NTProofStr = HMAC_MD5(NTOWFv2, ServerChallenge + blob)

Pour Azerty123, le hash NT vaut 9ab14788afc13c83576dfb13ac619152, et le NTOWFv2 pour user1 / TESTLOCAL vaut 5f9826e92806b709caaa263122f4e49c. Attention, seul le nom d’utilisateur est passé en majuscules, le domaine est pris tel que le client l’a envoyé.

Nous extrayons d’abord les valeurs avec tshark:

code
tshark -r ntlm.pcapng -Y "ntlmssp.messagetype == 0x00000002" -T fields -e ntlmssp.ntlmserverchallenge
tshark -r ntlm.pcapng -Y "ntlmssp.messagetype == 0x00000003" -T fields -e ntlmssp.auth.username -e ntlmssp.auth.domain -e ntlmssp.ntlmv2_response

Puis ce petit script Python fait la vérification:

code
import hashlib, hmac, sys

def md4(data):
    try:
        return hashlib.new("md4", data).digest()
    except ValueError:
        from Crypto.Hash import MD4
        return MD4.new(data).digest()

pwd, user, dom, schal, resp = sys.argv[1:6]
schal = bytes.fromhex(schal.replace(":", ""))
resp  = bytes.fromhex(resp.replace(":", ""))

nthash = md4(pwd.encode("utf-16le"))
ntowf  = hmac.new(nthash, (user.upper() + dom).encode("utf-16le"), hashlib.md5).digest()
proof  = hmac.new(ntowf, schal + resp[16:], hashlib.md5).digest()

print("NTProofStr calculée :", proof.hex())
print("NTProofStr capturée :", resp[:16].hex())
print("OK" if proof == resp[:16] else "KO")
shell
python3 ntproof.py Azerty123 user1 TESTLOCAL 3f1a9c62d84b07e5 a1c406a268ef03d0…
NTProofStr calculée : a1c406a268ef03d09905c99198bd4748
NTProofStr capturée : a1c406a268ef03d09905c99198bd4748
OK

Attention: sur les distributions récentes, OpenSSL 3 désactive MD4 dans son fournisseur par défaut et hashlib.new("md4") lève une erreur; le script bascule alors sur pycryptodome. Selon la version de tshark, les champs de type octets sortent avec ou sans «: », d’où le replace.

Ce que la capture livre à un attaquant

Le script que nous venons d’écrire est, à peu de chose près, le cœur d’un outil de cassage hors ligne. Il suffit de remplacer le mot de passe connu par un dictionnaire. Tout ce qui est nécessaire se trouve dans la capture: utilisateur, domaine, challenge serveur, NTProofStr et blob. Hashcat attend ces éléments sous la forme suivante (mode 5600, NetNTLMv2):

code
user1::TESTLOCAL:3f1a9c62d84b07e5:a1c406a268ef03d09905c99198bd4748:0101000000000000…

NTLMv2 ralentit l’attaque par rapport à NTLMv1, dont la réponse se ramène à du DES et se casse indépendamment de la complexité du mot de passe, mais un mot de passe faible comme Azerty123 tombera quand même en quelques instants. C’est ce que récupère un outil comme Responder lorsqu’il empoisonne LLMNR ou NBT-NS, comme nous l’avions vu dans l’article Protocoles NBT-NS, LLMNR et exploitation des failles.

Même sans casser quoi que ce soit, le CHALLENGE fournit gratuitement de la reconnaissance: noms NetBIOS et DNS du serveur, du domaine et de la forêt, version de build de Windows. Il suffit d’envoyer un NEGOTIATE à n’importe quel point d’entrée qui accepte NTLM pour obtenir ces informations.

Reste le relais: l’attaquant ne cherche plus le mot de passe, il retransmet les trois messages en temps réel vers un autre service. C’est là que le MIC et la signature jouent leur rôle. Sur SMB ou LDAP, une signature exigée rend le relais inutile, car l’attaquant ne possède pas la clé de session. La vulnérabilité CVE-2019-1040 (« Drop the MIC ») permettait justement de retirer ce MIC; les systèmes corrigés le refusent. Sur HTTP, la signature NTLM ne protège pas les requêtes qui suivent l’authentification: la parade est la protection étendue (EPA), qui lie l’authentification au canal TLS grâce à l’attribut MsvAvChannelBindings du blob. Sur notre maquette en HTTP, cet attribut n’a aucun sens et le site reste relayable.

Sur un parc réel, le filtre ntlmssp.messagetype == 0x00000003 && !ntlmssp.ntlmv2_response combiné à la stratégie « Envoyer uniquement les réponses NTLMv2. Refuser LM et NTLM » permet déjà de faire disparaître le NTLMv1. Pour le reste, je recommande d’activer EPA sur les sites IIS en authentification Windows et d’exiger la signature LDAP et SMB: une capture NTLMv2 ne servira alors plus qu’à tenter de casser le mot de passe hors ligne.

Rémi Vernier

Rémi Vernier

Responsable des opérations cybersécurité dans un grand groupe international. Identité, Active Directory, Entra ID, cloud hybride, et désormais SAP, IA et modèles en local.

Tous les articles → LinkedIn →

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *