From 65bb6c59fce2223830c1a6e9e4c12428b8eeccf6 Mon Sep 17 00:00:00 2001 From: David Fort Date: Thu, 4 Dec 2025 09:56:17 +0100 Subject: [PATCH] rdpear: handle basic NTLM commands and fix server-side This patch adds the handling of basic NTLM commands. Because there's some mysterious 4 zero bytes after pickle header in Kerberos packets, not present in NTLM commands, the patch also had to rework a bit the packet parsing / forging. The patch also addresses a server-side bug when parsing supplemental creds, if the client was sending an empty list, we were considering this as an error. And finally we also implement the parsing of MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL. This breaks the public API, anyway this was basically unused (as not parsed before) and the previous API was wrong as what we receive is MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL not MSV1_0_SUPPLEMENTAL_CREDENTIAL, so I guess the API breakage is ok. --- channels/rdpear/client/rdpear_main.c | 79 ++++++++++--- channels/rdpear/common/ndr.c | 3 +- .../common/rdpear-common/rdpear_common.h | 5 +- channels/rdpear/common/rdpear_common.c | 23 ++-- channels/rdpear/common/test/TestNdrEar.c | 3 +- include/freerdp/peer.h | 12 +- libfreerdp/core/nla.c | 108 ++++++++++++++++-- winpr/include/winpr/secapi.h | 31 +++++ 8 files changed, 219 insertions(+), 45 deletions(-) diff --git a/channels/rdpear/client/rdpear_main.c b/channels/rdpear/client/rdpear_main.c index 31b7cc25b..2145c2432 100644 --- a/channels/rdpear/client/rdpear_main.c +++ b/channels/rdpear/client/rdpear_main.c @@ -193,13 +193,13 @@ out: } static BOOL rdpear_send_payload(RDPEAR_PLUGIN* rdpear, IWTSVirtualChannelCallback* pChannelCallback, - RdpEarPackageType packageType, wStream* payload) + BOOL isKerb, wStream* payload) { GENERIC_CHANNEL_CALLBACK* callback = (GENERIC_CHANNEL_CALLBACK*)pChannelCallback; BOOL ret = FALSE; wStream* finalStream = NULL; SecBuffer cryptedBuffer = { 0 }; - wStream* unencodedContent = rdpear_encodePayload(packageType, payload); + wStream* unencodedContent = rdpear_encodePayload(isKerb, payload); if (!unencodedContent) goto out; @@ -248,7 +248,7 @@ out: return ret; } -static BOOL rdpear_prepare_response(NdrContext* rcontext, UINT16 callId, UINT32 status, +static BOOL rdpear_prepare_response(NdrContext* rcontext, BOOL isKerb, UINT16 callId, UINT32 status, NdrContext** pwcontext, wStream* retStream) { WINPR_ASSERT(rcontext); @@ -266,8 +266,17 @@ static BOOL rdpear_prepare_response(NdrContext* rcontext, UINT16 callId, UINT32 Stream_Write(retStream, payloadHeader, sizeof(payloadHeader)); if (!ndr_write_header(wcontext, retStream) || !ndr_start_constructed(wcontext, retStream) || - !ndr_write_pickle(wcontext, retStream) || /* pickle header */ - !ndr_write_uint16(wcontext, retStream, callId) || /* callId */ + !ndr_write_pickle(wcontext, retStream)) /* pickle header */ + goto out; + if (isKerb) + { + /* for some reason there's 4 zero undocumented bytes here after the pickle record + * in the kerberos package packets */ + UINT32 v = 0; + if (!ndr_write_uint32(wcontext, retStream, v)) + goto out; + } + if (!ndr_write_uint16(wcontext, retStream, callId) || /* callId */ !ndr_write_uint16(wcontext, retStream, 0x0000) || /* align padding */ !ndr_write_uint32(wcontext, retStream, status) || /* status */ !ndr_write_uint16(wcontext, retStream, callId) || /* callId */ @@ -748,8 +757,22 @@ out: return TRUE; } +static BOOL rdpear_ntlm_version(NdrContext* rcontext, wStream* s, UINT32* pstatus, UINT32* pversion) +{ + *pstatus = ERROR_INVALID_DATA; + + if (!ndr_read_uint32(rcontext, s, pversion)) + return TRUE; + + WLog_DBG(TAG, "-> NtlmNegotiateVersion(v=0x%x)", *pversion); + *pstatus = 0; + + return TRUE; +} + static UINT rdpear_decode_payload(RDPEAR_PLUGIN* rdpear, - IWTSVirtualChannelCallback* pChannelCallback, wStream* s) + IWTSVirtualChannelCallback* pChannelCallback, + const WinPrAsn1_OctetString* packageName, wStream* s) { UINT ret = ERROR_INVALID_DATA; NdrContext* context = NULL; @@ -761,7 +784,6 @@ static UINT rdpear_decode_payload(RDPEAR_PLUGIN* rdpear, CreateApReqAuthenticatorResp createApReqAuthenticatorResp = { 0 }; UnpackKdcReplyBodyResp unpackKdcReplyBodyResp = { 0 }; PackApReplyResp packApReplyResp = { 0 }; - void* resp = NULL; NdrMessageType respDescr = NULL; @@ -769,16 +791,40 @@ static UINT rdpear_decode_payload(RDPEAR_PLUGIN* rdpear, if (!respStream) goto out; - Stream_Seek(s, 16); /* skip first 16 bytes */ + BOOL isKerb = FALSE; + switch (rdpear_packageType_from_name(packageName)) + { + case RDPEAR_PACKAGE_KERBEROS: + isKerb = TRUE; + break; + case RDPEAR_PACKAGE_NTLM: + isKerb = FALSE; + break; + default: + WLog_ERR(TAG, "unknown package type"); + goto out; + } + Stream_Seek(s, 16); /* skip first 16 bytes */ wStream commandStream = { 0 }; UINT16 callId = 0; UINT16 callId2 = 0; context = ndr_read_header(s); if (!context || !ndr_read_constructed(context, s, &commandStream) || - !ndr_read_pickle(context, &commandStream) || - !ndr_read_uint16(context, &commandStream, &callId) || + !ndr_read_pickle(context, &commandStream)) + goto out; + + if (isKerb) + { + /* for some reason there's 4 zero undocumented bytes here after the pickle record + * in the kerberos package packets */ + UINT32 v = 0; + if (!ndr_read_uint32(context, &commandStream, &v)) + goto out; + } + + if (!ndr_read_uint16(context, &commandStream, &callId) || !ndr_read_uint16(context, &commandStream, &callId2) || (callId != callId2)) goto out; @@ -837,9 +883,12 @@ static UINT rdpear_decode_payload(RDPEAR_PLUGIN* rdpear, if (rdpear_kerb_PackApReply(rdpear, context, &commandStream, &status, &packApReplyResp)) ret = CHANNEL_RC_OK; break; - case RemoteCallNtlmNegotiateVersion: - WLog_ERR(TAG, "don't wanna support NTLM"); + resp = &uint32Resp; + respDescr = ndr_uint32_descr(); + + if (rdpear_ntlm_version(context, &commandStream, &status, &uint32Resp)) + ret = CHANNEL_RC_OK; break; default: @@ -849,7 +898,7 @@ static UINT rdpear_decode_payload(RDPEAR_PLUGIN* rdpear, break; } - if (!rdpear_prepare_response(context, callId, status, &wcontext, respStream)) + if (!rdpear_prepare_response(context, isKerb, callId, status, &wcontext, respStream)) goto out; if (resp && respDescr) @@ -870,7 +919,7 @@ static UINT rdpear_decode_payload(RDPEAR_PLUGIN* rdpear, } if (!ndr_end_constructed(wcontext, respStream) || - !rdpear_send_payload(rdpear, pChannelCallback, RDPEAR_PACKAGE_KERBEROS, respStream)) + !rdpear_send_payload(rdpear, pChannelCallback, isKerb, respStream)) { WLog_DBG(TAG, "rdpear_send_payload !!!!!!!!"); goto out; @@ -946,7 +995,7 @@ static UINT rdpear_on_data_received(IWTSVirtualChannelCallback* pChannelCallback wStream payloadStream = { 0 }; Stream_StaticInit(&payloadStream, payload.data, payload.len); - ret = rdpear_decode_payload(rdpear, pChannelCallback, &payloadStream); + ret = rdpear_decode_payload(rdpear, pChannelCallback, &packageName, &payloadStream); out: sspi_SecBufferFree(&decrypted); return ret; diff --git a/channels/rdpear/common/ndr.c b/channels/rdpear/common/ndr.c index 697d6742e..d318ed63c 100644 --- a/channels/rdpear/common/ndr.c +++ b/channels/rdpear/common/ndr.c @@ -216,7 +216,7 @@ BOOL ndr_read_pickle(NdrContext* context, wStream* s) if (!ndr_read_uint32(context, s, &v) || v != 0x20000) return FALSE; - return ndr_read_uint32(context, s, &v); // padding + return TRUE; } BOOL ndr_write_pickle(NdrContext* context, wStream* s) @@ -227,7 +227,6 @@ BOOL ndr_write_pickle(NdrContext* context, wStream* s) if (!ndr_write_uint32(context, s, 0x20000)) return FALSE; - ndr_write_uint32(context, s, 0); /* padding */ return TRUE; } diff --git a/channels/rdpear/common/rdpear-common/rdpear_common.h b/channels/rdpear/common/rdpear-common/rdpear_common.h index 1734548f5..e1b06b178 100644 --- a/channels/rdpear/common/rdpear-common/rdpear_common.h +++ b/channels/rdpear/common/rdpear-common/rdpear_common.h @@ -79,8 +79,9 @@ typedef enum // End NTLM remote calls } RemoteGuardCallId; -FREERDP_LOCAL RdpEarPackageType rdpear_packageType_from_name(WinPrAsn1_OctetString* package); -FREERDP_LOCAL wStream* rdpear_encodePayload(RdpEarPackageType packageType, wStream* payload); +FREERDP_LOCAL RdpEarPackageType rdpear_packageType_from_name(const WinPrAsn1_OctetString* package); +WINPR_ATTR_MALLOC(Stream_Free, 1) +FREERDP_LOCAL wStream* rdpear_encodePayload(BOOL isKerb, wStream* payload); #define RDPEAR_COMMON_MESSAGE_DECL(V) \ FREERDP_LOCAL BOOL ndr_read_##V(NdrContext* context, wStream* s, const void* hints, V* obj); \ diff --git a/channels/rdpear/common/rdpear_common.c b/channels/rdpear/common/rdpear_common.c index 616b83cc6..a781fa036 100644 --- a/channels/rdpear/common/rdpear_common.c +++ b/channels/rdpear/common/rdpear_common.c @@ -31,7 +31,7 @@ static char kerberosPackageName[] = { }; static char ntlmPackageName[] = { 'N', 0, 'T', 0, 'L', 0, 'M', 0 }; -RdpEarPackageType rdpear_packageType_from_name(WinPrAsn1_OctetString* package) +RdpEarPackageType rdpear_packageType_from_name(const WinPrAsn1_OctetString* package) { if (package->len == sizeof(kerberosPackageName) && memcmp(package->data, kerberosPackageName, package->len) == 0) @@ -44,7 +44,7 @@ RdpEarPackageType rdpear_packageType_from_name(WinPrAsn1_OctetString* package) return RDPEAR_PACKAGE_UNKNOWN; } -wStream* rdpear_encodePayload(RdpEarPackageType packageType, wStream* payload) +wStream* rdpear_encodePayload(BOOL isKerb, wStream* payload) { wStream* ret = NULL; WinPrAsn1Encoder* enc = WinPrAsn1Encoder_New(WINPR_ASN1_DER); @@ -57,18 +57,15 @@ wStream* rdpear_encodePayload(RdpEarPackageType packageType, wStream* payload) /* packageName [1] OCTET STRING */ WinPrAsn1_OctetString packageOctetString; - switch (packageType) + if (isKerb) { - case RDPEAR_PACKAGE_KERBEROS: - packageOctetString.data = (BYTE*)kerberosPackageName; - packageOctetString.len = sizeof(kerberosPackageName); - break; - case RDPEAR_PACKAGE_NTLM: - packageOctetString.data = (BYTE*)ntlmPackageName; - packageOctetString.len = sizeof(ntlmPackageName); - break; - default: - goto out; + packageOctetString.data = (BYTE*)kerberosPackageName; + packageOctetString.len = sizeof(kerberosPackageName); + } + else + { + packageOctetString.data = (BYTE*)ntlmPackageName; + packageOctetString.len = sizeof(ntlmPackageName); } if (!WinPrAsn1EncContextualOctetString(enc, 1, &packageOctetString)) diff --git a/channels/rdpear/common/test/TestNdrEar.c b/channels/rdpear/common/test/TestNdrEar.c index a9112b1c3..85baab14c 100644 --- a/channels/rdpear/common/test/TestNdrEar.c +++ b/channels/rdpear/common/test/TestNdrEar.c @@ -322,7 +322,7 @@ static int TestNdrEarRead(int argc, char* argv[]) }; size_t sizeofPayload4 = sizeof(payload4); #endif -#if 1 + size_t sizeofPayload4 = 0; BYTE* payload4 = parseHexBlock("03 01 03 01 \ 04 00 02 00 38 9e ef 6b 0c 00 02 00 18 00 02 00 \ @@ -346,7 +346,6 @@ static int TestNdrEarRead(int argc, char* argv[]) if (!payload4) goto out; -#endif CreateApReqAuthenticatorReq createApReqAuthenticatorReq = { 0 }; s = Stream_StaticInit(&staticS, payload4, sizeofPayload4); diff --git a/include/freerdp/peer.h b/include/freerdp/peer.h index db092281a..7b3ebf4d6 100644 --- a/include/freerdp/peer.h +++ b/include/freerdp/peer.h @@ -56,8 +56,18 @@ extern "C" typedef BOOL (*psPeerHasMoreToRead)(freerdp_peer* peer); typedef BOOL (*psPeerClose)(freerdp_peer* peer); typedef void (*psPeerDisconnect)(freerdp_peer* peer); + + /** callback called when we receive remote credential guard credentials during NLA + * @param peer the associated freerdp_peer + * @param logonCreds the KERB_TICKET_LOGON containing the TGT and the host service ticket + * @param suppCreds some MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL containing NTLM hashes + * @return if the treatment was successful + * @bug before 3.19.0 suppCreds were a pointer to MSV1_0_SUPPLEMENTAL_CREDENTIAL, not + * MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL as now + */ typedef BOOL (*psPeerRemoteCredentials)(freerdp_peer* peer, KERB_TICKET_LOGON* logonCreds, - MSV1_0_SUPPLEMENTAL_CREDENTIAL* suppCreds); + MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL* suppCreds); + typedef BOOL (*psPeerCapabilities)(freerdp_peer* peer); typedef BOOL (*psPeerPostConnect)(freerdp_peer* peer); typedef BOOL (*psPeerActivate)(freerdp_peer* peer); diff --git a/libfreerdp/core/nla.c b/libfreerdp/core/nla.c index 3875d97b7..2980a9551 100644 --- a/libfreerdp/core/nla.c +++ b/libfreerdp/core/nla.c @@ -1196,6 +1196,40 @@ static BOOL nla_read_KERB_TICKET_LOGON(WINPR_ATTR_UNUSED rdpNla* nla, wStream* s return TRUE; } +WINPR_ATTR_MALLOC(free, 1) +static MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL* nla_read_NtlmCreds(WINPR_ATTR_UNUSED rdpNla* nla, + wStream* s) +{ + WINPR_ASSERT(nla); + WINPR_ASSERT(s); + + if (!Stream_CheckAndLogRequiredLength(TAG, s, 32 + 4)) + return NULL; + + size_t pos = Stream_GetPosition(s); + Stream_Seek(s, 32); + + ULONG EncryptedCredsSize = Stream_Get_UINT32(s); + if (!Stream_CheckAndLogRequiredLength(TAG, s, EncryptedCredsSize)) + return NULL; + + Stream_SetPosition(s, pos); + + MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL* ret = (MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL*)calloc( + 1, sizeof(MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL) - 1 + EncryptedCredsSize); + if (!ret) + return NULL; + + ret->Version = Stream_Get_UINT32(s); + ret->Flags = Stream_Get_UINT32(s); + Stream_Read(s, ret->CredentialKey.Data, MSV1_0_CREDENTIAL_KEY_LENGTH); + ret->CredentialKeyType = Stream_Get_UINT32(s); + ret->EncryptedCredsSize = EncryptedCredsSize; + Stream_Read(s, ret->EncryptedCreds, EncryptedCredsSize); + + return ret; +} + /** @brief kind of RCG credentials */ typedef enum { @@ -1372,10 +1406,11 @@ static BOOL nla_read_ts_credentials(rdpNla* nla, SecBuffer* data) } /* supplementalCreds [1] SEQUENCE OF TSRemoteGuardPackageCred OPTIONAL, */ - MSV1_0_SUPPLEMENTAL_CREDENTIAL* suppCreds = NULL; + MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL* suppCreds = NULL; WinPrAsn1Decoder suppCredsSeq = { 0 }; - if (WinPrAsn1DecReadContextualSequence(&dec2, 1, &error, &suppCredsSeq)) + if (WinPrAsn1DecReadContextualSequence(&dec2, 1, &error, &suppCredsSeq) && + Stream_GetRemainingLength(&suppCredsSeq.source)) { WinPrAsn1Decoder ntlmCredsSeq = { 0 }; if (!WinPrAsn1DecReadSequence(&suppCredsSeq, &ntlmCredsSeq)) @@ -1393,7 +1428,12 @@ static BOOL nla_read_ts_credentials(rdpNla* nla, SecBuffer* data) return FALSE; } - /* TODO: suppCreds = &ntlmCreds; and parse NTLM creds */ + suppCreds = nla_read_NtlmCreds(nla, &ntlmPayload); + if (!suppCreds) + { + WLog_ERR(TAG, "invalid supplementalCreds"); + return FALSE; + } } else if (error) { @@ -1403,6 +1443,7 @@ static BOOL nla_read_ts_credentials(rdpNla* nla, SecBuffer* data) freerdp_peer* peer = nla->rdpcontext->peer; ret = IFCALLRESULT(TRUE, peer->RemoteCredentials, peer, &kerbLogon, suppCreds); + free(suppCreds); break; } default: @@ -1486,6 +1527,40 @@ out: return ret; } +static BOOL nla_write_TSRemoteGuardNtlmCred(rdpNla* nla, WinPrAsn1Encoder* enc, + const MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL* pntlm) +{ + WINPR_UNUSED(nla); + BOOL ret = FALSE; + BYTE ntlm[] = { 'N', '\0', 'T', '\0', 'L', '\0', 'M', '\0' }; + const WinPrAsn1_OctetString packageName = { sizeof(ntlm), ntlm }; + + /* packageName [0] OCTET STRING */ + if (!WinPrAsn1EncContextualOctetString(enc, 0, &packageName)) + return FALSE; + + /* credBuffer [1] OCTET STRING */ + wStream* s = Stream_New(NULL, 300); + if (!s) + goto out; + + Stream_Write_UINT32(s, pntlm->Version); /* Version */ + Stream_Write_UINT32(s, pntlm->Flags); /* Flags */ + + Stream_Write(s, pntlm->CredentialKey.Data, MSV1_0_CREDENTIAL_KEY_LENGTH); + Stream_Write_UINT32(s, pntlm->CredentialKeyType); + Stream_Write_UINT32(s, pntlm->EncryptedCredsSize); + Stream_Write(s, pntlm->EncryptedCreds, pntlm->EncryptedCredsSize); + Stream_Zero(s, 6 + 16 * 4 + 14); + + WinPrAsn1_OctetString credBuffer = { Stream_GetPosition(s), Stream_Buffer(s) }; + ret = WinPrAsn1EncContextualOctetString(enc, 1, &credBuffer) != 0; + +out: + Stream_Free(s, TRUE); + return ret; +} + /** * Encode TSCredentials structure. * @param nla A pointer to the NLA to use @@ -1665,13 +1740,26 @@ static BOOL nla_encode_ts_credentials(rdpNla* nla) if (!nla_write_TSRemoteGuardKerbCred(nla, enc) || !WinPrAsn1EncEndContainer(enc)) goto out; - /* supplementalCreds [1] SEQUENCE OF TSRemoteGuardPackageCred OPTIONAL, - * - * no NTLM supplemental creds for now - * - */ - if (!WinPrAsn1EncContextualSeqContainer(enc, 1) || !WinPrAsn1EncEndContainer(enc)) - goto out; + /* TODO: compute the NTLM supplemental creds */ + MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL* ntlm = NULL; + if (ntlm) + { + /* supplementalCreds [1] SEQUENCE OF TSRemoteGuardPackageCred OPTIONAL */ + if (!WinPrAsn1EncContextualSeqContainer(enc, 1)) + goto out; + + if (!WinPrAsn1EncSeqContainer(enc)) /* start NTLM */ + goto out; + + if (!nla_write_TSRemoteGuardNtlmCred(nla, enc, ntlm)) + goto out; + + if (!WinPrAsn1EncEndContainer(enc)) /* end NTLM */ + goto out; + + if (!WinPrAsn1EncEndContainer(enc)) /* supplementalCreds */ + goto out; + } /* End TSRemoteGuardCreds */ if (!WinPrAsn1EncEndContainer(enc)) diff --git a/winpr/include/winpr/secapi.h b/winpr/include/winpr/secapi.h index 6eccb4cbb..044e8345d 100644 --- a/winpr/include/winpr/secapi.h +++ b/winpr/include/winpr/secapi.h @@ -70,6 +70,37 @@ typedef struct #define MSV1_0_CRED_VERSION_REMOTE 0xffff0002 +typedef enum _MSV1_0_CREDENTIAL_KEY_TYPE +{ + InvalidCredKey, + DeprecatedIUMCredKey, + DomainUserCredKey, + LocalUserCredKey, + ExternallySuppliedCredKey +} MSV1_0_CREDENTIAL_KEY_TYPE; + +#define MSV1_0_CREDENTIAL_KEY_LENGTH 20 +#define MSV1_0_CRED_LM_PRESENT 0x1 +#define MSV1_0_CRED_NT_PRESENT 0x2 +#define MSV1_0_CRED_REMOVED 0x4 +#define MSV1_0_CRED_CREDKEY_PRESENT 0x8 +#define MSV1_0_CRED_SHA_PRESENT 0x10 + +typedef struct +{ + UCHAR Data[MSV1_0_CREDENTIAL_KEY_LENGTH]; +} MSV1_0_CREDENTIAL_KEY, *PMSV1_0_CREDENTIAL_KEY; + +typedef struct +{ + ULONG Version; + ULONG Flags; + MSV1_0_CREDENTIAL_KEY CredentialKey; + MSV1_0_CREDENTIAL_KEY_TYPE CredentialKeyType; + ULONG EncryptedCredsSize; + UCHAR EncryptedCreds[1]; +} MSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL, *PMSV1_0_REMOTE_SUPPLEMENTAL_CREDENTIAL; + #endif /* _WIN32 */ #ifndef KERB_LOGON_FLAG_REDIRECTED