Impacket et FreeIPA

Sur un pentest on peut de temps en temps tomber sur des belles personnes qui utilisent un FreeIPA plutôt qu'un Microslop ActiveDirectory très largement exploité et pour lequel il existe déjà une chiée d'outils. Oui mais FreeIPA c'est prévu pour être un remplacement de AD avec les mêmes protocoles à savoir LDAP et surtout Kerberos, et aussi d'autres services funky comme un équivalent de ADCS. Et là quand on essaye de taper dessus comme ~~un script kiddie~~ un⋅e gros⋅se bourrin⋅ne, ça marche moyennement.

Admettons qu'on ait déjà monté un FreeIPA dans un lab avec le domaine qui va bien, qu'il répond, etc.

# nmap -sS -sV ipatoast.orbital-chainsaw.test
Starting Nmap 7.92 ( https://nmap.org ) at 2026-06-30 11:38 CEST
Stats: 0:01:37 elapsed; 0 hosts completed (1 up), 1 undergoing Service Scan
Service scan Timing: About 87.50% done; ETC: 11:40 (0:00:13 remaining)
Nmap scan report for ipatoast.orbital-chainsaw.test (192.168.1.50)
Host is up (0.00012s latency).
Not shown: 982 filtered tcp ports (no-response), 10 filtered tcp ports (admin-prohibited)
PORT     STATE SERVICE         VERSION
22/tcp   open  ssh             OpenSSH 10.2 (protocol 2.0)
80/tcp   open  http            Apache httpd 2.4.68 ((Fedora Linux) OpenSSL/3.5.7 mod_wsgi/5.0.2 Python/3.14 mod_auth_gssapi/1.6.5)
88/tcp   open  kerberos-sec    MIT Kerberos (server time: 2026-06-30 09:39:10Z)
389/tcp  open  ldap            (Anonymous bind OK)
443/tcp  open  ssl/http        Apache httpd 2.4.68 ((Fedora Linux) OpenSSL/3.5.7 mod_wsgi/5.0.2 Python/3.14 mod_auth_gssapi/1.6.5)
464/tcp  open  kerberos-sec    MIT Kerberos (server time: 2026-06-30 09:39:10Z)
636/tcp  open  ssl/ldap        (Anonymous bind OK)
9090/tcp open  ssl/zeus-admin?
MAC Address: 52:54:00:AB:C5:01 (QEMU virtual NIC)
Service Info: Host: ORBITAL-CHAINSAW.TEST

Jusque là rien de choquant, c'est un FreeIPA tout frais.

Et de là on a l'irrépressible envie de faire un getTGT:

$ examples/getTGT.py -dc-ip 192.168.1.50 orbital-chainsaw.test/admin:FooBarBaz
Impacket v0.13.1 - Copyright Fortra, LLC and its affiliated companies

<TagSet object, tags 0:32:16-64:32:26> not in asn1Spec: <EncASRepPart schema object, tagSet=<TagSet object, tags 0:32:16-64:32:25>, subtypeSpec=<ConstraintsIntersection object>, componentType=<NamedTypes object, types <NamedType object, type key=<EncryptionKey schema object, tagSet=<TagSet object, tags 0:32:16-128:32:0>, subtypeSpec=<ConstraintsIntersection object>, componentType=<NamedTypes object, types <NamedType object, type keytype=<Int32 schema object, tagSet <TagSet object, tags 0:0:2-128:32:0>, subtypeSpec <ConstraintsIntersection object, consts <ValueRangeConstraint object, consts -2147483648, 2147483647>>>>, <NamedType object, type keyvalue=<OctetString schema object, tagSet <TagSet object, tags 0:0:4-128:32:1>, encoding iso-8859-1>>>, sizeSpec=<ConstraintsIntersection object>>>, <NamedType object, type last-req=<LastReq schema object, tagSet=<TagSet object, tags 0:32:16-128:32:1>, subtypeSpec=<ConstraintsIntersection object>, componentType=<Sequence schema object, tagSet=<TagSet object, tags 0:32:16>, subtypeSpec=<ConstraintsIntersection object>,[...]

C'est déjà embêtant. Impacket est sympa il nous dit déjà où est le problème. C'est pas un problème de XML contrairement à ce qu'on pourrait croire en lisant rapidement (ne demandez pas pourquoi cette idée subitement), c'est bien un problème de décodage de ASN1. On va quand même s'en sortir parce-qu'on a prévu d'installer krb5-client et qu'on peut faire ceci :

$ kinit admin@ORBITAL-CHAINSAW.TEST
Password for admin@ORBITAL-CHAINSAW.TEST:

$ klist -e
Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: admin@ORBITAL-CHAINSAW.TEST

Valid starting       Expires              Service principal
06/30/2026 05:01:49  07/01/2026 04:56:03  krbtgt/ORBITAL-CHAINSAW.TEST@ORBITAL-CHAINSAW.TEST
    Etype (skey, tkt): aes256-cts-hmac-sha1-96, camellia256-cts-cmac

Tadaaaa, et là y-a plus qu'à utiliser le token en spécifiant KRB5CCNAME, non ?

$ impacket-GetADUsers orbital-chainsaw.test/admin -no-pass -k -dc-ip 192.168.1.50 -dc-host ipatoast.orbital-chainsaw.test
Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies

[-] 26 is not a valid EncryptionTypes

Bien sûr que non. D'ailleurs ça vient bien de Impacket.:

$ impacket-describeTicket /tmp/krb5cc_1000
Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies

[*] Number of credentials in cache: 1
[*] Parsing credential[0]:
[-] 26 is not a valid EncryptionTypes

Pour info la liste des EncryptionTypes est dans krb5/constants.py :

class EncryptionTypes(Enum):
    des_cbc_crc                  = 1
    des_cbc_md4                  = 2
    des_cbc_md5                  = 3
    _reserved_4                  = 4
    des3_cbc_md5                 = 5
    _reserved_6                  = 6
    des3_cbc_sha1                = 7
    dsaWithSHA1_CmsOID           = 9
    md5WithRSAEncryption_CmsOID  = 10
    sha1WithRSAEncryption_CmsOID = 11
    rc2CBC_EnvOID                = 12
    rsaEncryption_EnvOID         = 13
    rsaES_OAEP_ENV_OID           = 14
    des_ede3_cbc_Env_OID         = 15
    des3_cbc_sha1_kd             = 16
    aes128_cts_hmac_sha1_96      = 17
    aes256_cts_hmac_sha1_96      = 18
    rc4_hmac                     = 23
    rc4_hmac_exp                 = 24
    subkey_keymaterial           = 65
    rc4_hmac_old_exp             = -135

Il nous manque ce 26, qui d'après krb5.h correspond à KRB5_ENCTYPE_CAMELLIA256_CTS_CMAC. Ça peut s'expliquer assez facilement par le fait qu'encore en 2026 et malgré l'existence de RFC 6803, AD DS ne supporte pas Camellia. La bonne nouvelle c'est que ça correspond bien à ce qui est présenté dans klist. Comme on a pas assez réfléchi la question, on va tenter de rajouter le support pour CAMELLIA256_CTS_CMAC dans Impacket, on sait jamais ça pourrait resservir. Au pire il restera toujours kinit, non ? (non).

Est-ce qu'on va se contenter de scripts un peu léchés pour énumérer tout l'annuaire ? Pour l'instant oui, ça va être important pour la suite. Pour ça on pourra aller faire un tour sur Hacktricks qui nous donne quelques idées de ce qu'on peut faire avec ldapsearch et PKINIT pour s'authentifier avec une clé privée.

On pourrait aussi modifier permitted_enctypes dans /etc/krb5.conf pour forcer l'utilisation de AES avec kinit, mais si ça ne marche toujours pas et que Camellia rescend quoi qu'il arrive, on continue.

En suivant le fil depuis describeTicket.py jusqu'à crypto.py on arrive aux méthodes string_to_key qui appartiennent aux classes attribuées en fonction de l'identifiant de l'algorithme utilisé. Jusque là y-a plus qu'à intégrer les primitives Camellia depuis la lib utilisée par Impacket qui n'est autre que PyCryptodome et-

Ah ben y-a pas. Et on remercie pas au passage le branleur qui a généré une page avec un LLM disant comment faire quand même et qui donne du code pour AES. On va pas non plus faire une implem de Camellia, pas maintenant, pas aujourd'hui. On va juste rajouter le package python-camelia et ça sera très bien pour la journée.

$ python -m pip install python-camellia
Collecting python-camellia
  Using cached python-camellia-1.0.tar.gz (27 kB)
  Installing build dependencies ... done
  Getting requirements to build wheel ... error
  error: subprocess-exited-with-error

  × Getting requirements to build wheel did not run successfully.
  │ exit code: 1
  ╰─> [27 lines of output]
  [...]
        File "/tmp/pip-install-ycu90j5e/python-camellia_22781b53b84542378385055701139581/src/camellia_build/_build_utils.py", line 8, in <module>
          from cffi import FFI
      ModuleNotFoundError: No module named 'cffi'
      [end of output]

  note: This error originates from a subprocess, and is likely not a problem with pip.

Ah ben merci pip de te décharger sur les autres mais ça ne résout ni tes problèmes ni les miens.

On va passer outre Camellia pour le moment, en espérant pas passer à côté de nouveautés de RFC6803.

On revient sur le décodage du AS_REP

pyasn1.error.PyAsn1Error: <TagSet object, tags 0:32:16-64:32:26> not in asn1Spec: <EncASRepPart schema object

Cette fonction est appelée dans kerberosv5.py

encASRepPart = decoder.decode(plainText, asn1Spec = EncASRepPart())[0]

Si on regarde le tag correspondant à EncASRepPart on a ceci :

class ApplicationTagNumbers(Enum):
    Ticket         = 1
    Authenticator  = 2
    EncTicketPart  = 3
    AS_REQ         = 10
    AS_REP         = 11
    TGS_REQ        = 12
    TGS_REP        = 13
    AP_REQ         = 14
    AP_REP         = 15
    RESERVED16     = 16
    RESERVED17     = 17
    KRB_SAFE       = 20
    KRB_PRIV       = 21
    KRB_CRED       = 22
    EncASRepPart   = 25
    EncTGSRepPart  = 26

Et si on intervertit avec EncTGSRepPart on avance un peu, mais guère plus loin. Est-ce qu'on aurait directement un TGS et il faudrait vérifier si on est dans les clous niveau protocole ? On va décortiquer getKerberosTGT dans kerberosv5.py et comparer avec ce qu'on peut lire chez Hackndo, voire comparer le trafic d'un kinit et d'un getTGT dans Wireshark tutututulu.

AS_REQ de kinit et impacket

Oui j'ai pris le screen à la fin de tout ça, jugez pas.

Si on met côte à côte les réponses AS_REP, à part l'utilisation de UDP par kinit et de TCP par Impacket, rien n'a l'air de sauter aux yeux. Evidemment il faut se pencher sur enc_part. En rajoutant une ligne de debug dans kerberosv5.py on peut obtenir un plaintext plus simple à interpréter:

7a81f03081eda02b3029a003020112a1220420831647e1cb7280bbaebbf2f5b8e5a23c4a84ccf56ebd18b35a8988bc0c9dada4a11c301a3018a003020100a111180f31393730303130313030303030305aa20602045cac96f6a311180f32303236303932383038343730335aa40703050050e10000a511180f32303236303730323039313934365aa711180f32303236303730333038353434345aa811180f32303236303730333039313934365aa9171b154f52424954414c2d434841494e5341572e54455354aa2a3028a003020101a121301f1b066b72627467741b154f52424954414c2d434841494e5341572e54455354

Application 26 (1 elem)
  SEQUENCE (10 elem)
    [0] (1 elem)
      SEQUENCE (2 elem)
Offset: 8
Length: 2+41
(constructed)
Value:
(2 elem)
        [0] (1 elem)
          INTEGER 18
        [1] (1 elem)

https://pkitools.net/pages/ca/asn1.html#eoHwMIHtoCswKaADAgESoSIEIIMWR-HLcoC7rrvy9bjlojxKhMz1br0Ys1qJiLwMna2koRwwGjAYoAMCAQChERgPMTk3MDAxMDEwMDAwMDBaogYCBFyslvajERgPMjAyNjA5MjgwODQ3MDNapAcDBQBQ4QAApREYDzIwMjYwNzAyMDkxOTQ2WqcRGA8yMDI2MDcwMzA4NTQ0NFqoERgPMjAyNjA3MDMwOTE5NDZaqRcbFU9SQklUQUwtQ0hBSU5TQVcuVEVTVKoqMCigAwIBAaEhMB8bBmtyYnRndBsVT1JCSVRBTC1DSEFJTlNBVy5URVNU

On retrouve bien le Tag 26, mais est-ce qu'on le retrouve pour autant dans la réponse donnée à kinit?

En suivant une présentation de Pass the SALT 2023 on se rappelle que Wireshark sait déchiffrer les échanges Kerberos pour peu qu'on lui donne une clé dans un fichier keytab.txt. Cette clé est dérivée du mot de passe et du sel. L'un est connu dans notre cas, l'autre se trouve dans le AS_REP.

>>> from binascii import hexlify
>>> from impacket.krb5.crypto import generate_kerberos_keys
>>> hexlify(generate_kerberos_keys(password='FooBarBaz', salt='^@w30dw=P/ew?e$,')[18].contents)
b'9bd196d2bdc526ea6e891899d4e417fe98c8cf71c0fa5813e358a1980d34975d'

On rentre ça dans un keytab, on le donne à Wireshark, et on peut lire les trames générées avec Impacket, mais toujours pas celles de kinit. C'est plutôt une bonne nouvelle, la clé générée par Impacket a l'air de faire le boulot.

On tente quand même de déchiffrer directement avec la lib impacket

>>> from binascii import hexlify, unhexlify
>>> from binascii import dehexlify
>>> from impacket.krb5.crypto import generate_kerberos_keys
>>> key = generate_kerberos_keys(password='FooBarBaz', salt='^@w30dw=P/ew?e$,')[18]
>>> enc_part = unhexlify('c2942c00c7b027bb41844394a1f2bddb168f039c15285740b7bb369a91d42c0fc340eb5f09288af9945c7d4baa8d5195264a35e98f1a28f7989982dd480e2e9574540af72e419f3c4eee4449ac6f8a0d8974e832e87877284581b3870636773925b5013ce4edbc79e6f470fecef0e924470dbac4a98db70dff89072c2584aa2b555d3b964a251f5a6eb1c8417435aa9e0ce85d2c1d733f8e6eef864b5ec5dd298b372d85bb0ae089462b2ad182991e158020077cc4055d1d3c9758f34fc5e7396e0408ddf02c2d23f8d1a33416dc3623f5d34eb27c894385f56fb1832cbfef16a8f4cc08f7aa76a8ad2518d580e2224405bc1e24fda5a9c2314b75a1507f0e5acdb1e03d3a2d10ad103577beaa2d09543097c22a899471e7defcf75cfad5088fe89dce3fd5e99e435546e8316b984427c48d2f3aa4fdcdbe14b978029f608cb9d1673287')
>>> cipher = _enctype_table[18]
>>> cipher.decrypt(key, 3, enc_part)
Traceback (most recent call last):
  File "<python-input-25>", line 1, in <module>
    cipher.decrypt(key, 3, enc_part)
    ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
  File "/home/jivay/Documents/boulot/code/impacket/impacket/krb5/crypto.py", line 240, in decrypt
    raise InvalidChecksum('ciphertext integrity failure')

Et évidemment la même chose avec le ciphertext reçu par Impacket et lu depuis Wireshark ça fonctionne. Il nous manque encore quelques infos.

Et si on arrêtait deux secondes d'essayer de déchiffrer ça et qu'on disait juste à Impacket d'accepter le tag 26 au-lieu de 25 ? Et moyennant aussi quelques modifications complémentaires pour que tout soit lu comme du 26

$ PYTHONPATH=~/impacket examples/getTGT.py orbital-chainsaw.test/admin:FooBarBaz -dc-ip 192.168.1.50 -debug

Impacket v0.14.0.dev0+20260619.174856.9a5621d4 - Copyright Fortra, LLC and its affiliated companies

[+] Impacket Library Installation Path: /home/jivay/impacket/impacket
[+] Trying to connect to KDC at 192.168.1.50:88
[+] Trying to connect to KDC at 192.168.1.50:88
[*] Saving ticket in admin.ccache
$ klist -e -c admin.ccache
Ticket cache: FILE:admin.ccache
Default principal: admin@ORBITAL-CHAINSAW.TEST

Valid starting       Expires              Service principal

On a créé un fichier, mais concrètement il ne servira à rien. Et en fait une fois qu'on a regardé les exemples de Impacket, la plupart tournent autour de SMB et de composants MS plus qu'autour de LDAP.

Ca aura permis de se faire une meilleure idée de comment fonctionne Kerberos.

links

social