Exploring NTLM_REMOTE_SUPPLEMENTAL_CREDENTIAL
Some time ago, I've been working on the kerberos part of remote credential guards, and after complains
about compatibility against windows 11 and recent DCs, I have looked at the NTLM part.
Remote credential guards
The idea behind remote credential guards is that we connect to a remote machine, the password is not sent but we still have SSO. So in theory, even if we connect to a compromised machine, we will not have our password stolen.
To achieve that, first in the CredSSP step of NLA some TSRemoteGuardPackageCred are sent. Inside you'll find a TGT for the Kerberos part and NTLM hashes. Next, the RDPEAR channel is used, and it allows the LSASS process of the RDP server to do remote calls on our local LSASS basically for all operations that need our precious password.
One could question this functionality, because although the compromised server doesn't have your password, with this LSASS remoting it can perform almost all the operations that requires your password. In addition to this, the server is also provided a valid TGT, so it can retrieve any TGS it wants. Given that, having the password in clear doesn't increase that much your privileges.
NTLM
In the TSRemoteGuardPackageCred packet, we also have NTLM_REMOTE_SUPPLEMENTAL_CREDENTIAL that contains NTLM hashes, it looks like:
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;
Unfortunately the reserved field is undocumented (I have an in progress case opened with dochelp@microsoft.com for this one), but the real subject of this
post is the CredentialKey field. The note in MS-CSSP says:
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.
Well, I'm not the super crypto expert. To summarize, it's computed from the user's password and SID (this way 2 users with the same password have different hashes). In python that gives:
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'
One would notice that the return of pbkdf2 is 32 bytes, but it is truncated to 16 bytes and 4 zero bytes are
appended to reach 20 bytes. I can imagine that before some md5 was involved (hash length 20 bytes), but to notify
that the new algorithm is used we have these trailing zeros.
Conclusion
I was suggested some last minute content on this subject, but unfortunately (or fortunately, you'll tell me), that's on the IT security side...