Aller au contenu

Accueil / IA frontier

Agent Skills Claude: un skill d’analyse Kerberos

Nous écrivons un Agent Skill Claude qui analyse une sortie klist: SKILL.md, script Python, références, chargement progressif et appel via l'API Claude.

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

Nous allons écrire sur notre maquette test.local un skill Claude capable de lire la sortie de « klist » d’un poste, de repérer le TGT et les tickets de service, puis de signaler ce qui pose problème: chiffrement RC4, délégation, flags inattendus, etc…

Pour le même travail, nous aurions aussi pu écrire un long prompt système qui embarque tout: la procédure, la table des types de chiffrement, les règles de détection. Le skill découpe ce contenu et ne le charge qu’au moment où l’agent en a besoin, et c’est ce découpage qui nous intéresse ici.

Présentation de la maquette

Nom du domaine: test.local (TESTLOCAL)

Contrôleur de domaine: DC01.test.local

Serveur web: srv01, site monsite.test.local exécuté par le compte svc.monsite@test.local

Utilisateur: alice@test.local

C’est la maquette de l’article sur IIS et Kerberos, avec le même compte de service. Depuis le poste d’alice, après un accès au site, nous récupérons la sortie de « klist »:

code
Current LogonId is 0:0x3e7a1

Cached Tickets: (2)

#0>	Client: alice @ TEST.LOCAL
	Server: krbtgt/TEST.LOCAL @ TEST.LOCAL
	KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
	Ticket Flags 0x40e10000 -> forwardable renewable initial pre_authent name_canonicalize
	Start Time: 11/8/2026 9:02:11 (local)
	End Time:   11/8/2026 19:02:11 (local)
	Renew Time: 11/15/2026 9:02:11 (local)
	Session Key Type: AES-256-CTS-HMAC-SHA1-96
	Cache Flags: 0x1 -> PRIMARY
	Kdc Called: DC01.test.local

#1>	Client: alice @ TEST.LOCAL
	Server: HTTP/monsite.test.local @ TEST.LOCAL
	KerbTicket Encryption Type: RSADSI RC4-HMAC(NT)
	Ticket Flags 0x40a10000 -> forwardable renewable pre_authent name_canonicalize
	Start Time: 11/8/2026 9:05:43 (local)
	End Time:   11/8/2026 19:02:11 (local)
	Renew Time: 11/15/2026 9:02:11 (local)
	Session Key Type: RSADSI RC4-HMAC(NT)
	Cache Flags: 0
	Kdc Called: DC01.test.local

Nous avions déjà lu ce type de sortie à la main dans l’article Kerberos: lire un ticket de service avec klist et Wireshark. Cette fois, nous allons confier la lecture à Claude, en lui fournissant une méthode fixe plutôt que de le laisser improviser.

Structure du skill

Un skill est un simple répertoire. Seul le fichier SKILL.md est obligatoire, le reste dépend de ce que nous voulons y mettre. Voici l’arborescence de notre skill:

code
analyse-kerberos/
├── SKILL.md
├── scripts/
│   └── parse_klist.py
└── references/
    ├── etypes.md
    └── detection.md

1. Le fichier SKILL.md

Le fichier commence par un en-tête YAML avec deux champs obligatoires, « name » et « description ». Le nom est limité à 64 caractères (minuscules, chiffres et tirets) et doit correspondre au nom du répertoire. La description peut aller jusqu’à 1024 caractères. Microsoft documente ce même format pour son Agent Framework: un skill écrit pour Claude n’est donc pas enfermé chez un seul éditeur.

code
---
name: analyse-kerberos
description: Analyse une sortie klist Windows (tickets Kerberos en cache) ou des champs Kerberos extraits avec tshark. Identifie TGT et tickets de service, types de chiffrement, flags et durées, et signale les configurations à risque (RC4, DES, ok-as-delegate). À utiliser dès qu'une sortie klist, un ticket Kerberos ou une erreur KRB5 est fournie.
---

# Analyse Kerberos

## Démarche
1. Enregistrer la sortie fournie dans /tmp/klist.txt puis lancer
   python scripts/parse_klist.py < /tmp/klist.txt
   Ne jamais décoder les flags ou les etypes à la main.
2. Travailler à partir du JSON produit: une entrée par ticket, avec ses alertes.
3. Pour un type de chiffrement ou un flag inconnu, lire references/etypes.md.
4. Si un ticket de service est en RC4 ou DES, lire references/detection.md.

## Format de réponse
- Un tableau: serveur, etype du ticket, etype de session, flags, alertes.
- Les constats, du plus grave au moins grave, chacun avec l'attribut AD
  ou le paramètre à vérifier.

## Règles
- Si l'entrée contient un ticket complet (blob base64, KRB_CRED, .kirbi, keytab),
  ne pas l'analyser et demander la sortie klist ou les champs tshark.
- Ne jamais proposer de méthode pour casser ou rejouer un ticket.

La description est la partie la plus travaillée du skill: c’est la seule que l’agent voit en permanence, c’est donc elle qui décide si le skill sera ouvert. Une description vague du type « outils Kerberos » laissera Claude répondre de mémoire sans jamais charger la méthode.

2. Le script

Le décodage d’un champ « Ticket Flags » en hexadécimal est une opération déterministe. Le modèle sait la faire, mais rien ne garantit qu’il la fasse bien à chaque fois. Nous la confions donc à un script:

code
#!/usr/bin/env python3
"""Analyse une sortie klist Windows et produit un JSON."""
import json, re, sys

FLAGS = {
    0x40000000: "forwardable", 0x20000000: "forwarded",
    0x10000000: "proxiable",   0x08000000: "proxy",
    0x00800000: "renewable",   0x00400000: "initial",
    0x00200000: "pre_authent", 0x00040000: "ok_as_delegate",
    0x00010000: "name_canonicalize",
}
FAIBLES = ("RC4", "DES")

def champ(bloc, nom):
    m = re.search(rf"{nom}:\s*(.+)", bloc)
    return m.group(1).strip() if m else ""

def analyse(texte):
    tickets = []
    for bloc in re.split(r"#\d+>", texte)[1:]:
        m = re.search(r"Ticket Flags\s+(0x[0-9a-fA-F]+)", bloc)
        f = int(m.group(1), 16) if m else 0
        t = {
            "client": champ(bloc, "Client"),
            "serveur": champ(bloc, "Server"),
            "etype_ticket": champ(bloc, "KerbTicket Encryption Type"),
            "etype_session": champ(bloc, "Session Key Type"),
            "flags": [n for v, n in FLAGS.items() if f & v],
            "kdc": champ(bloc, "Kdc Called"),
            "alertes": [],
        }
        if any(x in t["etype_ticket"] for x in FAIBLES):
            t["alertes"].append("ticket chiffré avec un etype faible")
        if "ok_as_delegate" in t["flags"] and not t["serveur"].startswith("krbtgt/"):
            t["alertes"].append("service approuvé pour la délégation (ok-as-delegate)")
        tickets.append(t)
    return tickets

if __name__ == "__main__":
    print(json.dumps(analyse(sys.stdin.read()), ensure_ascii=False, indent=2))

Sur notre sortie, le script renvoie pour le ticket #1:

json
{
  "client": "alice @ TEST.LOCAL",
  "serveur": "HTTP/monsite.test.local @ TEST.LOCAL",
  "etype_ticket": "RSADSI RC4-HMAC(NT)",
  "etype_session": "RSADSI RC4-HMAC(NT)",
  "flags": ["forwardable", "renewable", "pre_authent", "name_canonicalize"],
  "kdc": "DC01.test.local",
  "alertes": ["ticket chiffré avec un etype faible"]
}

Attention: le script attend les libellés anglais de « klist ». Si vos postes renvoient une sortie localisée, il faudra adapter les expressions régulières.

3. Les références

Le fichier etypes.md contient la table des types de chiffrement (1 et 3 pour DES, 17 pour aes128-cts-hmac-sha1-96, 18 pour aes256-cts-hmac-sha1-96, 23 pour rc4-hmac) et les bits de l’attribut msDS-SupportedEncryptionTypes (0x4 pour RC4, 0x8 pour AES128, 0x10 pour AES256). Le fichier detection.md décrit ce qu’il faut chercher côté contrôleur de domaine. Nous y reviendrons plus bas.

Chargement progressif ou prompt système?

Le skill est chargé en trois niveaux. Au démarrage, seuls le nom et la description de chaque skill sont injectés dans le contexte, de l’ordre d’une centaine de tokens par skill. Quand une demande correspond à la description, Claude lit le corps de SKILL.md. Les références et les scripts ne viennent qu’ensuite, et seulement si la démarche les appelle.

Niveau 1 name + description toujours dans le contexte Niveau 2 corps de SKILL.md lu si la demande correspond Niveau 3 references/ et scripts/ lus ou exécutés au besoin parse_klist.py seul le JSON revient dans le contexte Prompt système monolithique procédure + tables + règles envoyées à chaque requête
Chargement progressif du skill analyse-kerberos

La documentation d’Anthropic insiste sur un point pour les scripts: Claude les exécute avec bash, ce qui donne des opérations déterministes sans charger leur code dans le contexte. Notre parseur ne coûte donc que la taille de sa sortie JSON.

Avec un prompt monolithique, la procédure, la table des etypes et les règles de détection partent à chaque requête, même pour une question qui n’a rien à voir avec Kerberos, et c’est le modèle lui-même qui convertit 0x40a10000 en liste de flags.

Le skill a lui aussi un coût: l’outil d’exécution de code ajoute sa définition et les échanges bash au contexte, et le corps de SKILL.md est relu à chaque déclenchement. Sur une seule question, le gain en tokens n’est donc pas acquis d’avance. Pour le mesurer, il suffit de passer le même jeu de sorties « klist » dans les deux configurations et de comparer usage.input_tokens et usage.output_tokens dans les réponses de l’API. Pour la fiabilité, nous comparons les tableaux produits à la sortie du script, qui sert de référence.

Déposer le skill et l’appeler

À l’heure où j’écris ces lignes (novembre 2026), l’API Skills est sortie de bêta et ne demande plus d’en-tête beta. L’ancien en-tête anthropic-beta: skills-2025-10-02 fonctionne toujours mais renvoie les anciennes structures de réponse. Côté SDK Python, nous passons par client.skills au lieu de client.beta.skills.

1. Envoi du skill

shell
curl https://api.anthropic.com/v1/skills \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -F "display_title=Analyse Kerberos" \
  -F "files[]=@analyse-kerberos/SKILL.md;filename=analyse-kerberos/SKILL.md" \
  -F "files[]=@analyse-kerberos/scripts/parse_klist.py;filename=analyse-kerberos/scripts/parse_klist.py" \
  -F "files[]=@analyse-kerberos/references/etypes.md;filename=analyse-kerberos/references/etypes.md" \
  -F "files[]=@analyse-kerberos/references/detection.md;filename=analyse-kerberos/references/detection.md"

La réponse contient l’identifiant du skill (préfixe skill_) et sa version (préfixe skver_ sans l’en-tête beta). Attention, vérifiez la forme exacte de la requête multipart dans la documentation correspondant à votre version d’API.

2. Appel de l’API Messages

Pour que Claude utilise le skill, nous listons son skill_id dans le paramètre container et nous ajoutons l’outil d’exécution de code, puisque c’est son conteneur qui héberge les skills. D’après la page de l’outil d’exécution de code, aucune des trois versions actuelles ne demande d’en-tête anthropic-beta.

code
import anthropic

client = anthropic.Anthropic()
klist = open("klist_alice.txt", encoding="utf-8").read()

r = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=4096,
    container={"skills": [
        {"type": "custom", "skill_id": "skill_01...", "version": "latest"}
    ]},
    tools=[{"type": "code_execution_20260521", "name": "code_execution"}],
    messages=[{"role": "user",
               "content": "Analyse cette sortie klist du poste d'alice:\n\n" + klist}],
)

print(r.usage.input_tokens, r.usage.output_tokens)
for bloc in r.content:
    print(bloc.type)

En parcourant r.content, nous voyons la démarche de l’agent bloc par bloc:

-> server_tool_use: lecture de SKILL.md dans le conteneur

-> server_tool_use: écriture de /tmp/klist.txt puis lancement de parse_klist.py

-> résultat bash: le JSON du script

-> server_tool_use: lecture de references/detection.md, déclenchée par l’alerte RC4

-> text: le tableau et les constats

Si le bloc text arrive sans aucune lecture de SKILL.md avant lui, la description n’a pas déclenché le skill et c’est elle que nous retravaillons.

Attention: « latest » convient pour la mise au point. En production, nous fixons un numéro de version, sinon une modification du skill change le comportement de l’agent sans que l’appel ait changé.

Ce que le skill signale, et ce qu’il ne doit pas voir

Le constat principal sur notre maquette porte sur le ticket de service HTTP/monsite.test.local, chiffré en RC4-HMAC. Nous avions déjà rencontré le même cas en fin d’article sur la configuration de IIS pour Kerberos. La partie chiffrée de ce ticket l’est avec une clé RC4, c’est-à-dire le hash NT du mot de passe de svc.monsite.

N’importe quel utilisateur authentifié du domaine peut demander un ticket pour ce SPN, l’emporter et tenter de retrouver le mot de passe hors ligne, sans plus jamais toucher le contrôleur de domaine. C’est le Kerberoasting, référencé T1558.003 dans MITRE ATT&CK. Un compte de service utilisateur a souvent un mot de passe ancien qui n’expire pas, et souvent des droits bien au-delà du site qu’il fait tourner: une fois le mot de passe retrouvé, l’attaquant ouvre des sessions avec ce compte partout où il est autorisé, et rien n’est révoqué tant que le mot de passe ne change pas.

Le fichier detection.md donne à l’agent les éléments pour orienter la recherche côté DC: l’événement 4769 (demande de ticket de service) avec un « Ticket Encryption Type » à 0x17. La difficulté, c’est qu’une demande de ticket est une opération normale. Un 4769 en RC4 isolé peut venir d’une vieille application légitime. Ce qui doit alerter, c’est un même compte qui demande des tickets RC4 pour de nombreux SPN différents en peu de temps, ou une demande RC4 sur un compte dont msDS-SupportedEncryptionTypes annonce AES.

Côté durcissement, le skill renvoie vers les bons leviers: positionner msDS-SupportedEncryptionTypes à 0x18 (AES128 et AES256) sur svc.monsite, changer son mot de passe pour générer les clés AES, ou mieux le remplacer par un gMSA dont le mot de passe est aléatoire et renouvelé automatiquement. Il suffit ensuite d’un « klist purge » sur le poste, d’un nouvel accès au site et d’un nouveau passage dans le skill pour vérifier que le ticket #1 est en AES-256-CTS-HMAC-SHA1-96.

Reste le skill lui-même. C’est du code et des instructions que l’agent suit: un skill récupéré sans relecture peut orienter les réponses ou exécuter autre chose que ce qu’annonce sa description. Nous le relisons comme n’importe quel script avant de le déposer. Sur l’API, les skills tournent dans un conteneur isolé sans accès réseau et sans installation de paquets, ce qui limite les dégâts. Dans Claude Code, un skill posé dans ~/.claude/skills/ exécute ses scripts sur le poste, avec les droits et le réseau de l’utilisateur.

Il faut aussi faire attention aux données envoyées. Une sortie « klist » ne contient pas de secret, mais elle révèle des noms de comptes, des SPN et le nom des contrôleurs de domaine. Une capture réseau complète, elle, contient les KRB_TGS_REP avec la partie chiffrée des tickets: c’est la matière première du Kerberoasting, qui n’a rien à faire dans une requête API. C’est pour cela que SKILL.md refuse les tickets complets. Si nous partons d’une capture, nous extrayons localement les seuls champs utiles avant de les transmettre:

code
tshark -r capture.pcapng -Y "kerberos.msg_type == 13" -T fields \
  -e frame.time -e kerberos.CNameString -e kerberos.SNameString -e kerberos.etype

Le champ kerberos.etype peut sortir plusieurs valeurs séparées par des virgules (etype du ticket et etype de la partie chiffrée de la réponse), ce que references/etypes.md explique à l’agent. Ces quatre colonnes suffisent pour repérer un ticket de service en 23, sans qu’aucun octet chiffré ne quitte le poste d’analyse.

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 *