Hardening consulting

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...