Un petit tour des NTLM_REMOTE_SUPPLEMENTAL_CREDENTIAL
Il y a quelques temps j'ai travaillé sur la partie Kerberos de remote credential guards, et suite à des
rapports de compatibilité avec du windows 11 et des controlleurs de domaine avec des versions
récentes, je me suis un peu intéressé à la partie NTLM.
Remote credential guards
L'idée derrière remote credential guards, c'est qu'on se connecte à une machine distante, le mot de passe n'est pas envoyé à cette machine, mais qu'on a quand même du SSO. Et donc en théorie, même si on se connecte à une machine compromise, on ne se fait pas voler son mot de passe.
Pour obtenir cet effet, premièrement dans la phase CredSSP de NLA vont être envoyé des TSRemoteGuardPackageCred. Dedans en gros on va avoir un TGT pour la partie Kerberos, et les hashes NTLM. Dans un deuxième temps, le channel dynamique RDPEAR va être ouvert, et va permettre au processus LSASS du serveur RDP de faire des appels à notre LSASS local pour toutes les opérations qui nécessiteraient notre précieux mot de passe.
Il est permis de se poser quelques questions sur cette fonctionnalité car certe le serveur compromis n'a pas notre mot de passe, mais avec ce LSASS remoting il peut faire quasiment toutes les opérations qu'on ferait si on l'avait. De plus, ce serveur éventuellement compromis a aussi un TGT valide pour notre user donc il peut se récupérer tous les TGS qu'il veut. Dans ces conditions, avoir le mot de passe ne rajoute pas tant de privilèges supplémentaires (sauf à l'utiliser sur des services externes vu que souvent c'est le même).
NTLM
Dans ce paquet TSRemoteGuardPackageCred, on a aussi des NTLM_REMOTE_SUPPLEMENTAL_CREDENTIAL qui contiennent les hashs NTLM, la structure ressemble à ça:
typedef struct _NTLM_REMOTE_SUPPLEMENTAL_CREDENTIAL { ULONG Version; ULONG Flags; MSV1_0_CREDENTIAL_KEY CredentialKey; MSV1_0_CREDENTIAL_KEY_TYPE CredentialKeyType; ULONG reservedsize; [size_is(reservedSize)] UCHAR* reserved; } NTLM_REMOTE_SUPPLEMENTAL_CREDENTIAL;
Le champs reserved n'est hélas pas documenté (j'ai une question en cours à dochelp@microsoft.com sur le sujet), mais le sujet de ce billet
c'est le champs CredentialKey. La doc dit qu'il s'agit de
The credential key is a 20-byte length unsigned char (UCHAR [MS-DTYP] section 2.2.45) array and is
calculated from the user's password as follows:
The NTOWF of the user is calculated from the password as described in [MS-NLMP] section 3.3.1.
The previous NTOWF result is then used to obtain a 32-byte length intermediate key using the PBKDF2
function ([RFC2898] section 5.2) with the NTOWF as the password, the SID of the user in UNICODE_STRING
format as the salt, SHA256 as the hash algorithm, and an iteration count of 10,000.
The final 16-byte key is calculated by running one iteration of PBKDF2 with the intermediate key as the
password, the SID of the user in UNICODE_STRING format as the salt, and SHA256 as the hash algorithm.
The last four bytes MUST be zeroed.
J'ai eu un peu de mal. En gros, c'est calculé à partir du mot de passe et du SID de l'utilisateur (ça permet que 2 utilisateurs avec le même mot de passe n'aient pas les mêmes hash). L'équivalent en python c'est:
import hashlib import hmac def unicodify(s): return s.encode('utf-16')[2:] def MSV1_0_CREDENTIAL_KEY(passwd, sid): h = hashlib.new("md4", unicodify(passwd)) # NTOWFv1 sid = unicodify(sid) v = hashlib.pbkdf2_hmac('sha256', h.digest(), salt=sid, iterations=10 * 1000) return hashlib.pbkdf2_hmac('sha256', v, salt=sid, iterations=1)[0:16] + b'\0\0\0\0'
On remarquera cette bizarrie: le retour de pbkdf2 est sur 32 octets, mais on le tronque à 16 octets, et on rajoute
4 octets à 0. J'imagine que c'est parce qu'avant, c'était du md5 qui intervenait (on avait
un résultat sur 20 octets directement), ces 4 octets à zéro permettent sans doute d'identifier qu'il s'agit du nouvel algo ?
Conclusion
J'ai trouvé du contenu de dernière minute concernant ce sujet, mais c'est malheureusement (ou heureusement, vous me direz...), sur le pan de la sécurité...