[PATCH 2/2] ksmbd: put the workgroup into the NTLMSSP target info's domain names

From: Jan Koester

Date: Sun Oct 18 2026 - 13:54:11 EST


ksmbd_build_ntlmssp_challenge_blob() writes the netbios name into all
four AV pairs of the challenge's target info, the domain names
(MsvAvNbDomainName, MsvAvDnsDomainName) included. The client copies
them into its NTLMv2 response.

With NTLM pass-through, a domain controller verifies that response over
NETLOGON, and Samba (NTLMv2_RESPONSE_verify_workstation()) refuses it
with NT_STATUS_LOGON_FAILURE unless MsvAvNbComputerName is the secure
channel's computer account and MsvAvNbDomainName its workgroup. Every
pass-through login of a domain user then fails.

Put the workgroup into the two domain names. Size the challenge blob
from both names in one helper, ntlmssp_challenge_size(), and check that
the blob fits into the response buffer when it is built in place
without SPNEGO: the workgroup may be longer than the netbios name.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Jan Koester <jan.koester@xxxxxxx>
---
fs/smb/server/auth.c | 41 ++++++++++++++++++++++++++++++++++-------
fs/smb/server/smb2pdu.c | 21 +++++++++++++++++++--
2 files changed, 53 insertions(+), 9 deletions(-)

diff --git a/fs/smb/server/auth.c b/fs/smb/server/auth.c
index 9a2a3c6a37..8639997080 100644
--- a/fs/smb/server/auth.c
+++ b/fs/smb/server/auth.c
@@ -376,10 +376,10 @@ ksmbd_build_ntlmssp_challenge_blob(struct challenge_message *chgblob,
struct ksmbd_conn *conn)
{
struct target_info *tinfo;
- wchar_t *name;
+ wchar_t *name, *domain;
__u8 *target_name;
unsigned int flags, blob_off, blob_len, type, target_info_len = 0;
- int len, uni_len, conv_len;
+ int len, uni_len, dom_len, conv_len;
int cflags = conn->ntlmssp.client_flags;

memcpy(chgblob->Signature, NTLMSSP_SIGNATURE, 8);
@@ -426,6 +426,23 @@ ksmbd_build_ntlmssp_challenge_blob(struct challenge_message *chgblob,

uni_len = UNICODE_LEN(conv_len);

+ len = strlen(ksmbd_work_group());
+ domain = kmalloc(2 + UNICODE_LEN(len), KSMBD_DEFAULT_GFP);
+ if (!domain) {
+ kfree(name);
+ return -ENOMEM;
+ }
+
+ conv_len = smb_strtoUTF16((__le16 *)domain, ksmbd_work_group(), len,
+ conn->local_nls);
+ if (conv_len < 0 || conv_len > len) {
+ kfree(domain);
+ kfree(name);
+ return -EINVAL;
+ }
+
+ dom_len = UNICODE_LEN(conv_len);
+
blob_off = sizeof(struct challenge_message);
blob_len = blob_off + uni_len;

@@ -446,14 +463,23 @@ ksmbd_build_ntlmssp_challenge_blob(struct challenge_message *chgblob,
tinfo = (struct target_info *)(target_name + uni_len);

chgblob->TargetInfoArray.Length = 0;
- /* Add target info list for NetBIOS/DNS settings */
+ /*
+ * Add target info list for NetBIOS/DNS settings: the computer names
+ * are the netbios name, the domain names the workgroup. A domain
+ * controller checks the latter when it is asked to verify the
+ * NTLMv2 response of a domain user (NETLOGON pass-through).
+ */
for (type = NTLMSSP_AV_NB_COMPUTER_NAME;
type <= NTLMSSP_AV_DNS_DOMAIN_NAME; type++) {
+ bool is_domain = type == NTLMSSP_AV_NB_DOMAIN_NAME ||
+ type == NTLMSSP_AV_DNS_DOMAIN_NAME;
+ int av_len = is_domain ? dom_len : uni_len;
+
tinfo->Type = cpu_to_le16(type);
- tinfo->Length = cpu_to_le16(uni_len);
- memcpy(tinfo->Content, name, uni_len);
- tinfo = (struct target_info *)((char *)tinfo + 4 + uni_len);
- target_info_len += 4 + uni_len;
+ tinfo->Length = cpu_to_le16(av_len);
+ memcpy(tinfo->Content, is_domain ? domain : name, av_len);
+ tinfo = (struct target_info *)((char *)tinfo + 4 + av_len);
+ target_info_len += 4 + av_len;
}

/* Add terminator subblock */
@@ -464,6 +490,7 @@ ksmbd_build_ntlmssp_challenge_blob(struct challenge_message *chgblob,
chgblob->TargetInfoArray.Length = cpu_to_le16(target_info_len);
chgblob->TargetInfoArray.MaximumLength = cpu_to_le16(target_info_len);
blob_len += target_info_len;
+ kfree(domain);
kfree(name);
ksmbd_debug(AUTH, "NTLMSSP SecurityBufferLength %d\n", blob_len);
return blob_len;
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 6b8809f67b..704c694fe7 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -1822,6 +1822,20 @@ static __le32 deassemble_neg_contexts(struct ksmbd_conn *conn,
return status;
}

+/*
+ * ntlmssp_challenge_size() - room for the challenge blob: the netbios name as
+ * target name and the two computer names, the workgroup as the two domain
+ * names (UTF-16, each with its 4-byte AV pair header), the terminator
+ */
+static size_t ntlmssp_challenge_size(void)
+{
+ size_t nb = strlen(ksmbd_netbios_name()) * 2;
+ size_t wg = strlen(ksmbd_work_group()) * 2;
+
+ return sizeof(struct challenge_message) + nb + 2 * (4 + nb) +
+ 2 * (4 + wg) + 4;
+}
+
/**
* smb2_handle_negotiate() - handler for smb2 negotiate command
* @work: smb work containing smb request buffer
@@ -2096,6 +2110,10 @@ static int ntlm_negotiate(struct ksmbd_work *work,
memset(chgblob, 0, sizeof(struct challenge_message));

if (!work->conn->use_spnego) {
+ if ((char *)rsp->Buffer + ntlmssp_challenge_size() >
+ (char *)work->response_buf + work->response_sz)
+ return -EINVAL;
+
sz = ksmbd_build_ntlmssp_challenge_blob(chgblob, work->conn);
if (sz < 0)
return -ENOMEM;
@@ -2104,8 +2122,7 @@ static int ntlm_negotiate(struct ksmbd_work *work,
return 0;
}

- sz = sizeof(struct challenge_message);
- sz += (strlen(ksmbd_netbios_name()) * 2 + 1 + 4) * 6;
+ sz = ntlmssp_challenge_size();

neg_blob = kzalloc(sz, KSMBD_DEFAULT_GFP);
if (!neg_blob)
--
2.56.0