Kerberos: lire un ticket de service avec klist et Wireshark, du KRB_TGS_REQ au PAC validé
L’objectif de cet article n’est pas de réexpliquer Kerberos de A à Z. Il existe déjà beaucoup de très bons schémas sur les trois têtes du chien. Ce qui manque souvent, c’est le lien entre ce schéma théorique et ce que l’on voit réellement sur le réseau, dans le cache du poste client et dans le journal de sécurité du contrôleur de domaine. Quand un site web en authentification intégrée retombe en NTLM sans prévenir, ou qu’un service renvoie un « KRB_AP_ERR_MODIFIED », c’est pourtant ce lien qui permet de trouver la cause en quelques minutes.
Nous allons voir dans cet article comment lire un ticket de service de bout en bout: la requête KRB_TGS_REQ, la réponse KRB_TGS_REP, l’AP-REQ présentée au service et la validation du PAC. Pour chaque étape je donne le filtre Wireshark et la commande qui permettent de la refaire sur votre propre maquette.
Présentation de la maquette
La maquette est volontairement minimale:
- Nom du domaine: test.local (royaume Kerberos TEST.LOCAL)
- dc01.test.local: contrôleur de domaine Windows Server 2025, 192.168.0.10
- srv01.test.local: serveur membre IIS, 192.168.0.20, qui héberge le site monsite.test.local
- cli01.test.local: poste Windows 11 sur lequel l’utilisateur testuser01 est connecté
- svc.monsite@test.local: compte de service utilisé comme identité du pool d’application IIS
Le SPN du site est porté par le compte de service et non par le compte machine srv01$. Ce point est très important pour la suite, puisque c’est lui qui détermine avec quelle clé le ticket sera chiffré. Côté préparation, cela donne:
# Sur dc01
New-ADUser -Name svc.monsite -SamAccountName svc.monsite `
-UserPrincipalName svc.monsite@test.local `
-AccountPassword (Read-Host -AsSecureString) -Enabled $true
# -S vérifie l'absence de doublon avant d'ajouter le SPN (contrairement à -A)
setspn -S HTTP/monsite.test.local TESTsvc.monsite
# AES128 + AES256 uniquement (0x18)
Set-ADUser svc.monsite -Replace @{'msDS-SupportedEncryptionTypes'=24}
# Sur srv01: le pool tourne sous TESTsvc.monsite, authentification Windows activée
%windir%system32inetsrvappcmd set config "monsite" -section:system.webServer/security/authentication/windowsAuthentication /useAppPoolCredentials:true /commit:apphost
Le paramètre useAppPoolCredentials indique à IIS de déchiffrer les tickets avec la clé de l’identité du pool plutôt qu’avec celle du compte machine, ce qui est nécessaire avec l’authentification en mode noyau. Sans lui, le scénario d’erreur que nous verrons en fin d’article apparaît directement.
Une remarque sur les etypes. À l’heure où j’écris ces lignes (octobre 2026), Microsoft a terminé le retrait par défaut de RC4 côté KDC. D’après l’article de support Microsoft lié à la CVE-2026-20833, la phase d’audit a commencé avec les mises à jour du 13 janvier 2026, l’application avec retour arrière manuel le 14 avril 2026, et les mises à jour de juillet 2026 suppriment la prise en charge de la valeur de registre RC4DefaultDisablementPhase. Le message MC1427604 confirme qu’il ne reste que le mode application, RC4 n’étant plus possible que par configuration explicite. Sur une maquette à jour, il ne devrait donc plus y avoir d’etype 23 dans les tickets de service, sauf si vous l’avez réactivé volontairement.
Petit rappel sur l’échange TGS
Le poste client possède déjà un TGT obtenu à l’ouverture de session (échange AS). Pour accéder à http://monsite.test.local, il doit obtenir un ticket pour le SPN HTTP/monsite.test.local, puis le présenter au serveur.
Capture réseau
Commençons par vider le cache du client pour forcer une nouvelle requête TGS. Sans cela, le ticket déjà en cache est réutilisé et vous ne verrez que l’AP-REQ.
klist purge
Attention, klist purge supprime aussi le TGT. Le client refera alors un échange AS avant le TGS, ce qui n’est pas gênant et nous donne au passage l’AS-REP dans la capture.
Si Wireshark est installé sur cli01, une capture avec le filtre de capture port 88 or port 80 suffit. Si vous ne voulez rien installer, pktmon fait le travail nativement depuis Windows 10 2004 et sait produire un pcapng:
pktmon filter add KRB -p 88
pktmon filter add HTTP -p 80
pktmon start --capture --pkt-size 0 --file-name C:tempkrb.etl
# Dans une autre console, on déclenche l'accès
Invoke-WebRequest -Uri http://monsite.test.local -UseDefaultCredentials
pktmon stop
pktmon etl2pcap C:tempkrb.etl -o C:tempkrb.pcapng
pktmon filter remove
J’utilise Invoke-WebRequest plutôt qu’un navigateur pour éviter les questions de zone Intranet: un nom avec des points comme monsite.test.local est vu comme Internet par défaut et Edge n’enverra pas forcément ses identifiants. Autre possibilité, klist get HTTP/monsite.test.local déclenche uniquement le TGS-REQ, sans contacter le serveur web. C’est pratique pour isoler le dialogue avec le KDC.
Une fois le fichier ouvert dans Wireshark, voici les filtres d’affichage que j’utilise:
kerberos tout le trafic Kerberos
kerberos.msg_type == 12 || kerberos.msg_type == 13 TGS-REQ / TGS-REP
kerberos.SNameString == "monsite.test.local" tout ce qui concerne le SPN
kerberos.msg_type == 14 AP-REQ (y compris dans HTTP/SPNEGO)
kerberos.msg_type == 30 KRB-ERROR
kerberos.error_code == 41 KRB_AP_ERR_MODIFIED
Les noms de champs peuvent changer légèrement d’une version de Wireshark à l’autre. En cas de doute, un clic droit sur le champ dans le détail du paquet puis « Appliquer comme filtre » donne le nom exact.
Lecture du KRB_TGS_REQ
Le TGS-REQ (msg-type 12) se compose de deux blocs: les padata, qui prouvent l’identité du client, et le req-body, qui décrit ce qu’il demande. Dans Wireshark, l’arborescence a cette forme (valeurs de la maquette, à adapter):
tgs-req
pvno: 5
msg-type: krb-tgs-req (12)
padata: 2 items
PA-DATA pA-TGS-REQ (1)
ap-req
ticket <- le TGT
realm: TEST.LOCAL
sname: krbtgt/TEST.LOCAL
enc-part
etype: eTYPE-AES256-CTS-HMAC-SHA1-96 (18)
kvno: 2 <- version de clé du compte krbtgt
cipher: ...
authenticator
etype: eTYPE-AES256-CTS-HMAC-SHA1-96 (18)
cipher: ... <- chiffré avec la clé de session du TGT
PA-DATA pA-PAC-OPTIONS (167)
req-body
kdc-options: 40810000 (forwardable, renewable, canonicalize)
realm: TEST.LOCAL
sname
name-type: kRB5-NT-SRV-INST (2)
sname-string: 2 items
SNameString: HTTP
SNameString: monsite.test.local
till: ...
nonce: ...
etype: 3 items (18, 17, 23...)
Trois éléments retiennent l’attention:
- Le sname du req-body est le SPN demandé, en clair. C’est lui que le KDC va rechercher dans l’annuaire (attribut
servicePrincipalName). Si le client construit un mauvais SPN (alias DNS, port non standard, etc…), c’est ici qu’on le voit. - La liste
etypedu req-body est celle que le client accepte. Elle sert au choix de la clé de session, pas à celui du chiffrement du ticket. Le chiffrement du ticket dépend du compte qui porte le SPN (msDS-SupportedEncryptionTypes, ou à défaut la politique du domaine). On confond souvent les deux. - Le TGT est présenté tel quel, chiffré avec la clé du compte krbtgt. Le kvno qu’on lit dans son enc-part est celui de krbtgt, pas celui du service.
Lecture du KRB_TGS_REP
La réponse (msg-type 13) contient deux parties chiffrées avec deux clés différentes. C’est le point qui pose le plus de questions à la lecture d’une capture, d’où le schéma ci dessous.
Dans Wireshark:
tgs-rep
pvno: 5
msg-type: krb-tgs-rep (13)
crealm: TEST.LOCAL
cname: user01
ticket
tkt-vno: 5
realm: TEST.LOCAL
sname: HTTP/monsite.test.local
enc-part
etype: eTYPE-AES256-CTS-HMAC-SHA1-96 (18)
kvno: 3 <- dépend de l'historique du compte
cipher: ...
enc-part
etype: eTYPE-AES256-CTS-HMAC-SHA1-96 (18)
cipher: ...
-> L’etype du ticket (18 ici) est celui que le KDC a choisi pour le compte de service. Avec msDS-SupportedEncryptionTypes à 0x18, on doit voir 17 ou 18, jamais 23.
-> Le kvno du ticket correspond à l’attribut msDS-KeyVersionNumber du compte qui porte le SPN. Il s’incrémente à chaque changement de mot de passe. C’est un indice précieux: il dit pour quel compte et pour quelle version de clé le ticket a été chiffré.
Get-ADUser svc.monsite -Properties msDS-KeyVersionNumber | Select-Object Name, msDS-KeyVersionNumber
Get-ADComputer srv01 -Properties msDS-KeyVersionNumber | Select-Object Name, msDS-KeyVersionNumber
Si le kvno lu dans la capture correspond à svc.monsite, le KDC a fait son travail. Si un service n’arrive pas à déchiffrer ce ticket, le problème est côté serveur (mauvaise identité, keytab périmé…).
Correspondance avec klist et l’événement 4769
Côté client, klist affiche le ticket obtenu. La sortie a cette forme:
#1> Client: user01 @ TEST.LOCAL
Server: HTTP/monsite.test.local @ TEST.LOCAL
KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
Ticket Flags 0x40a10000 -> forwardable renewable pre_authent name_canonicalize
Start Time: ...
End Time: ...
Renew Time: ...
Session Key Type: AES-256-CTS-HMAC-SHA1-96
Cache Flags: 0
Kdc Called: dc01.test.local
Côté contrôleur de domaine, chaque délivrance de ticket de service génère un événement 4769 « A Kerberos service ticket was requested », à condition que la sous-catégorie soit auditée. On le vérifie et on interroge le journal ainsi:
auditpol /get /subcategory:"Kerberos Service Ticket Operations"
Get-WinEvent -LogName Security -MaxEvents 5 -FilterXPath `
"*[System[EventID=4769] and EventData[Data[@Name='ServiceName']='svc.monsite']]" |
Format-List TimeCreated, Message
Le piège classique: dans le 4769, le champ « Service Name » ne contient pas le SPN mais le compte qui le porte (svc.monsite, ou SRV01$ pour un compte machine). Wireshark montre le SPN demandé, le 4769 montre le compte trouvé par le KDC. Mettre les deux côte à côte suffit souvent à démasquer un SPN mal placé.
| Information | Wireshark | klist | Événement 4769 |
|---|---|---|---|
| Service demandé | req-body / sname (SPN) | Server | Service Name (compte) |
| Chiffrement du ticket | ticket / enc-part / etype | KerbTicket Encryption Type | Ticket Encryption Type (0x11, 0x12, 0x17) |
| Clé de session | choisie parmi les etypes du req-body | Session Key Type | champs étendus (voir ci dessous) |
| Options | kdc-options (0x40810000) | Ticket Flags (0x40a10000) | Ticket Options |
| Version de clé | ticket / enc-part / kvno | non affichée | non affichée |
| Erreur KDC | KRB-ERROR / error-code | message de klist get |
Failure Code |
Pour le décodage: 0x11 correspond à AES128 (etype 17), 0x12 à AES256 (18) et 0x17 à RC4-HMAC (23). Les options demandées (0x40810000: forwardable, renewable, canonicalize) diffèrent des flags du ticket délivré (0x40a10000), car le KDC ajoute pre_authent. Concernant les champs étendus, cette fiche sur l’événement 4769 indique que depuis la mise à jour de janvier 2025, Windows Server 2016 et ultérieurs produisent une version 2 de l’événement avec, entre autres, les etypes annoncés par le client, le type de la clé de session et les clés disponibles pour le demandeur, le service et le DC. Je n’ai pas trouvé de source Microsoft plus précise sur le format exact. Le plus simple reste de vérifier sur vos propres DC ce que contient l’onglet « Détails » de l’événement.
Déchiffrer le ticket avec un keytab
Jusqu’ici, le contenu du ticket reste opaque. Pour lire l’EncTicketPart et le PAC, il faut fournir à Wireshark la clé du compte de service sous forme de keytab. Le wiki Wireshark indique que le fichier se déclare dans les préférences du protocole KRB5, ou en ligne de commande avec l’option -K, répétable pour charger plusieurs keytabs. Il précise aussi que les anciennes versions Windows avaient des limitations, corrigées depuis.
Sur la maquette, je génère le keytab de svc.monsite depuis une machine Linux avec le ktutil du MIT, en indiquant le kvno lu précédemment:
$ ktutil
ktutil: addent -password -p svc.monsite@TEST.LOCAL -k 3 -e aes256-cts-hmac-sha1-96
Password for svc.monsite@TEST.LOCAL:
ktutil: wkt svc.monsite.keytab
ktutil: quit
$ klist -kte svc.monsite.keytab
La clé AES dépend d’un sel. Pour un compte utilisateur AD, ce sel est construit à partir du royaume en majuscules et du nom du compte, ce qui correspond au sel par défaut de ktutil pour le principal svc.monsite@TEST.LOCAL. Si le compte a été renommé, ou si vous visez un compte machine (sel différent), la clé produite sera fausse et Wireshark ne déchiffrera rien. Une alternative, décrite dans cet article du blog Tenable, consiste à utiliser le script keytab.py de Dirk-jan Mollema, qui construit le keytab directement à partir des clés brutes en hexadécimal, sans mot de passe. L’auteur y précise que le nom du principal ou du domaine dans le keytab importe peu pour Wireshark.
Ensuite, dans « Edit » > « Preferences » > « Protocols » > « KRB5 », cochez « Try to decrypt Kerberos blobs » et indiquez le fichier keytab. En ligne de commande:
tshark -r krb.pcapng -K svc.monsite.keytab -o kerberos.decrypt:TRUE -Y "kerberos.msg_type == 13" -V
Une fois la clé connue, le ticket se déplie dans le TGS-REP comme dans l’AP-REQ. Si vous ajoutez au keytab la clé de user01, Wireshark peut aussi déchiffrer l’AS-REP, en déduire la clé de session du TGT, puis l’authentificateur du TGS-REQ et la partie client du TGS-REP. C’est très parlant en formation, mais un keytab contenant des clés de comptes reste un secret: avec la clé de svc.monsite, on peut forger des tickets pour ce service.
Le PAC et sa validation
Dans l’EncTicketPart déchiffré, le champ authorization-data contient un élément AD-IF-RELEVANT (1) qui encapsule un AD-WIN2K-PAC (128). C’est le PAC décrit dans la spécification [MS-PAC]. Wireshark en affiche les buffers, avec des libellés qui varient selon les versions:
- LOGON_INFO (1): SID de l’utilisateur, groupes, etc…
- CLIENT_INFO (10) et UPN_DNS_INFO (12)
- SERVER_CHECKSUM (6): signature calculée avec la clé du service
- PRIVSVR_CHECKSUM (7): signature calculée avec la clé du compte krbtgt
- sur un DC à jour, des buffers ajoutés au fil des correctifs de 2021 et 2022 (signature du ticket, attributs du PAC, demandeur, signature complète du PAC)
Le service vérifie forcément la signature serveur, puisqu’il possède la clé qui a servi à la calculer. En revanche il ne peut pas vérifier seul la signature KDC, qui repose sur la clé de krbtgt. C’est le rôle de la validation du PAC: le serveur transmet cette signature au contrôleur de domaine via le canal sécurisé Netlogon (message KERB_VERIFY_PAC), et le DC répond si elle est valide. Ce sont les étapes 4 et 5 du premier schéma.
Malheureusement cette validation n’a pas lieu à chaque fois. D’après la documentation Microsoft historique, elle est ignorée lorsque le processus qui accepte le contexte dispose du privilège « Agir en tant que partie du système d’exploitation » (SeTcbPrivilege), ce qui est le cas des services lancés sous SYSTEM. Le comportement a évolué au fil des versions et des correctifs PAC, et je ne le considère pas comme garanti pour un processus donné. Pour savoir si srv01 valide réellement le PAC, il faut regarder le trafic. Sur la capture prise côté serveur, filtrez:
ip.addr == 192.168.0.10 && (netlogon || dcerpc)
Un appel Netlogon de srv01 vers dc01 juste après l’AP-REQ indique une validation. Le contenu est généralement chiffré par le canal sécurisé, on ne voit donc que l’appel. Du point de vue sécurité, ce point compte: un ticket forgé avec la seule clé du service (silver ticket) porte une signature serveur valide et une signature KDC fausse. Seule la validation du PAC le détecte, et elle est souvent absente.
Cas d’erreur: KRB_AP_ERR_MODIFIED
Cassons maintenant la maquette de la façon la plus courante. Deux variantes produisent le même symptôme:
- Quelqu’un ajoute le SPN HTTP/monsite.test.local sur le compte machine srv01 avec
setspn -A, qui ne vérifie pas les doublons. Le SPN existe alors sur deux comptes. - Le pool d’application repasse en ApplicationPoolIdentity, ou
useAppPoolCredentialsest retiré. Le SPN reste sur svc.monsite, mais IIS tente de déchiffrer avec la clé de srv01$.
Dans les deux cas, le ticket est chiffré avec la clé d’un compte et le service essaie de le déchiffrer avec celle d’un autre. Le serveur renvoie l’erreur 41 (0x29), KRB_AP_ERR_MODIFIED, définie dans la RFC 4120. Dans la capture:
-> Le TGS-REQ et le TGS-REP sont normaux. Pas d’erreur côté KDC, et donc un 4769 avec un « Failure Code » à 0x0. C’est logique: le DC a bien délivré un ticket.
-> L’AP-REQ part dans l’en-tête « Authorization: Negotiate » et le serveur répond par un nouveau 401. Selon l’implémentation, l’en-tête « WWW-Authenticate » de cette réponse peut transporter un KRB-ERROR, visible avec kerberos.error_code == 41.
-> Le client enregistre dans le journal Système un événement 4 de la source Security-Kerberos, qui mentionne l’erreur KRB_AP_ERR_MODIFIED et le nom de la cible.
-> Si NTLM est autorisé, le navigateur retente en NTLM et l’utilisateur ne voit rien. C’est ce qui rend ce problème si discret en production.
Pour la variante du doublon, le contrôleur de domaine enregistre aussi un événement 11 de la source KDC dans son journal Système, qui signale plusieurs comptes portant le même SPN. Le comportement exact du KDC face au doublon (refus ou choix d’un des comptes) n’est pas quelque chose que je considère comme garanti. L’événement 11 est le signal fiable. Côté diagnostic, on combine ce que nous avons vu:
# Qui porte le SPN?
setspn -Q HTTP/monsite.test.local
# Tous les doublons de la forêt
setspn -X -F
# Pour quel compte le ticket a-t-il été chiffré? Comparer avec le kvno de la capture
Get-ADUser svc.monsite -Properties msDS-KeyVersionNumber
Get-ADComputer srv01 -Properties msDS-KeyVersionNumber
Le keytab rend aussi service ici. Si le keytab de svc.monsite déchiffre le ticket de l’AP-REQ, le KDC a chiffré pour le bon compte et c’est la configuration de srv01 qu’il faut corriger. S’il ne le déchiffre pas, le ticket a été émis pour un autre compte et c’est le SPN qu’il faut corriger. Le même raisonnement vaut pour un service Linux dont le keytab n’a pas suivi un changement de mot de passe: le kvno de la capture ne correspond plus à celui du fichier.
Une fois le SPN en double supprimé (setspn -D HTTP/monsite.test.local srv01) ou l’identité du pool corrigée, n’oubliez pas le client: il garde en cache le ticket fautif jusqu’à son expiration, dix heures par défaut. Un klist purge sur cli01, ou une fermeture de session, est nécessaire avant de retester. Enfin, supprimez le keytab généré pour la maquette dès que vous n’en avez plus besoin.