Loading src/smb.cpp +149 −0 Changes for src/smb.cpp: 149 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -205,6 +205,27 @@ struct SpnegoAuthenResponseMsg { uint64_t session_expiry; }; // ksmbd_ntlm_authen_request/response: a domain user's NTLM response, checked // by its DC over NETLOGON (adconnectd); for AD users marked UserPassthrough struct NtlmAuthenRequestMsg { uint32_t handle; unsigned char challenge[8]; uint16_t user_name_len; uint16_t domain_name_len; uint16_t workstation_len; uint16_t nt_response_len; uint16_t lm_response_len; uint16_t reserved16; uint32_t reserved[16]; // payload: user name, domain name, workstation (UTF16LE), NT, LM responses }; struct NtlmAuthenResponseMsg { uint32_t handle; uint32_t status; // UserOk or UserBadPassword unsigned char session_key[16]; uint32_t reserved[16]; }; // the kernel accepts a response only at exactly its struct's size static_assert(sizeof(StartupRequestMsg) == 744, "ksmbd_startup_request"); static_assert(sizeof(LoginRequestMsg) == 116, "ksmbd_login_request"); Loading @@ -216,6 +237,8 @@ static_assert(sizeof(TreeConnectRequestMsg) == 264, "ksmbd_tree_connect_request" static_assert(sizeof(TreeConnectResponseMsg) == 72, "ksmbd_tree_connect_response"); static_assert(sizeof(RpcCommandMsg) == 12, "ksmbd_rpc_command"); static_assert(sizeof(SpnegoAuthenResponseMsg) == 168, "ksmbd_spnego_authen_response"); static_assert(sizeof(NtlmAuthenRequestMsg) == 88, "ksmbd_ntlm_authen_request"); static_assert(sizeof(NtlmAuthenResponseMsg) == 88, "ksmbd_ntlm_authen_response"); enum : uint16_t { UserOk = 1 << 0, Loading Loading @@ -650,6 +673,7 @@ void SmbServer::handle(int cmd, const unsigned char *payload, size_t len) { case TreeConnectRequest: treeConnect(payload, len); break; case RpcRequest: rpc(payload, len); break; case SpnegoAuthenRequest: spnego(payload, len); break; case NtlmAuthenRequest: ntlmAuthen(payload, len); break; default: break; // heartbeat, tree disconnect, logout: nothing to answer } } Loading Loading @@ -681,6 +705,18 @@ void SmbServer::login(const unsigned char *payload, size_t len) { if (acct.idR || groupsOfR(acct).size() > 1) resp.status |= UserExtension; #else if (groupsOf(acct.name.c_str(), acct.gid).size() > 1) resp.status |= UserExtension; #endif copy(resp.account, sizeof(resp.account), acct.name); } else if (found && !root && !locked(acct.name.c_str()) && adconnectPresent()) { // an AD user has no local NT hash: tell ksmbd to pass the NTLM // response through to the domain controller (NTLM_AUTHEN_REQUEST) resp.uid = acct.uid; resp.gid = acct.gid; resp.status = UserOk | UserPassthrough; #ifdef SHAREDD_UID_R if (acct.idR || groupsOfR(acct).size() > 1) resp.status |= UserExtension; #else if (groupsOf(acct.name.c_str(), acct.gid).size() > 1) resp.status |= UserExtension; #endif copy(resp.account, sizeof(resp.account), acct.name); } else if (!found && _cfg.settings.mapToGuest == "bad_user") { Loading Loading @@ -938,6 +974,79 @@ bool askAdconnect(const unsigned char *blob, size_t len, KrbLogin &out, std::str return true; } bool adconnectPresent() { struct stat st; return stat(SHAREDD_ADCONNECT_SOCKET, &st) == 0 && S_ISSOCK(st.st_mode); } // a UTF-16LE name from the kernel as an 8-bit string (ASCII/Latin-1: the low // byte of each unit -- AD account and domain names are effectively ASCII) std::string utf16leLow(const unsigned char *p, size_t bytes) { std::string out; for (size_t i = 0; i + 1 < bytes; i += 2) out += (char)p[i]; return out; } // have adconnectd check a domain user's NTLM response at the DC (NETLOGON). // true with the 16-byte base session key on success; false if the password // was refused or adconnectd couldn't be reached (*error set). bool ntlmViaAdconnect(const std::string &domain, const std::string &account, const std::string &workstation, const unsigned char challenge[8], const std::string &nt, const std::string &lm, std::string &sessionKey, std::string &error) { int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); if (fd < 0) { error = strerror(errno); return false; } sockaddr_un a{}; a.sun_family = AF_UNIX; snprintf(a.sun_path, sizeof a.sun_path, "%s", SHAREDD_ADCONNECT_SOCKET); struct timeval tv = {30, 0}; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); if (connect(fd, (sockaddr *)&a, sizeof a) != 0) { error = std::string("adconnectd isn't running (") + strerror(errno) + ")"; close(fd); return false; } /* six uint16-length-prefixed fields: domain, account, workstation, * server challenge, NT response, LM response */ std::string payload; auto fld = [&](const std::string &s) { payload += (char)(s.size() & 0xff); payload += (char)((s.size() >> 8) & 0xff); payload += s; }; fld(domain); fld(account); fld(workstation); fld(std::string((const char *)challenge, 8)); fld(nt); fld(lm); std::string req = "NTLM " + base64((const unsigned char *)payload.data(), payload.size()) + "\n", reply; bool ok = ::send(fd, req.data(), req.size(), MSG_NOSIGNAL) == (ssize_t)req.size(); char buf[8192]; for (ssize_t n; ok && (n = recv(fd, buf, sizeof buf, 0)) > 0;) reply.append(buf, (size_t)n); close(fd); if (reply.compare(0, 4, "ERR ") == 0) { error = reply.substr(4, reply.find('\n') - 4); return false; } /* N <status> <base64 session key> */ size_t nl = reply.find('\n'); if (reply.compare(0, 2, "N\t") != 0 || nl == std::string::npos) { error = "bad answer from adconnectd"; return false; } std::vector<std::string> f; std::string line = reply.substr(2, nl - 2); size_t p = 0; for (size_t q; (q = line.find('\t', p)) != std::string::npos; p = q + 1) f.push_back(line.substr(p, q - p)); f.push_back(line.substr(p)); if (f.size() != 2) { error = "bad answer from adconnectd"; return false; } unsigned long status = strtoul(f[0].c_str(), nullptr, 10); if (status != 0) { error = "the domain controller refused the password"; return false; } if (!unbase64(f[1], sessionKey)) { error = "bad answer from adconnectd"; return false; } return true; } } // namespace // Kerberos logins (domain members asking for cifs/<this system>): the Loading Loading @@ -997,4 +1106,44 @@ void SmbServer::spnego(const unsigned char *payload, size_t len) { send(SpnegoAuthenResponse, msg.data(), msg.size(), false); } // NTLM logins of domain users (no Kerberos): ksmbd forwards the client's // response here for an account marked UserPassthrough; adconnectd has its // domain controller check it over NETLOGON and returns the session key. void SmbServer::ntlmAuthen(const unsigned char *payload, size_t len) { NtlmAuthenResponseMsg resp; memset(&resp, 0, sizeof(resp)); resp.status = UserBadPassword; if (len < sizeof(NtlmAuthenRequestMsg)) { send(NtlmAuthenResponse, &resp, sizeof(resp), false); return; } NtlmAuthenRequestMsg req; memcpy(&req, payload, sizeof(req)); resp.handle = req.handle; size_t off = sizeof(NtlmAuthenRequestMsg); size_t need = (size_t)req.user_name_len + req.domain_name_len + req.workstation_len + req.nt_response_len + req.lm_response_len; if (off + need > len) { send(NtlmAuthenResponse, &resp, sizeof(resp), false); return; } const unsigned char *p = payload + off; std::string user = utf16leLow(p, req.user_name_len); p += req.user_name_len; std::string domain = utf16leLow(p, req.domain_name_len); p += req.domain_name_len; std::string ws = utf16leLow(p, req.workstation_len); p += req.workstation_len; std::string nt((const char *)p, req.nt_response_len); p += req.nt_response_len; std::string lm((const char *)p, req.lm_response_len); p += req.lm_response_len; std::string key, error; if (ntlmViaAdconnect(domain, user, ws, req.challenge, nt, lm, key, error)) { resp.status = UserOk; memcpy(resp.session_key, key.data(), std::min<size_t>(key.size(), sizeof(resp.session_key))); std::fill(key.begin(), key.end(), '\0'); } else { std::cerr << "sharedd: SMB: NTLM login for " << domain << "\\" << user << " refused: " << error << std::endl; } send(NtlmAuthenResponse, &resp, sizeof(resp), false); } } // namespace sharedd Loading
src/smb.cpp +149 −0 Changes for src/smb.cpp: 149 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -205,6 +205,27 @@ struct SpnegoAuthenResponseMsg { uint64_t session_expiry; }; // ksmbd_ntlm_authen_request/response: a domain user's NTLM response, checked // by its DC over NETLOGON (adconnectd); for AD users marked UserPassthrough struct NtlmAuthenRequestMsg { uint32_t handle; unsigned char challenge[8]; uint16_t user_name_len; uint16_t domain_name_len; uint16_t workstation_len; uint16_t nt_response_len; uint16_t lm_response_len; uint16_t reserved16; uint32_t reserved[16]; // payload: user name, domain name, workstation (UTF16LE), NT, LM responses }; struct NtlmAuthenResponseMsg { uint32_t handle; uint32_t status; // UserOk or UserBadPassword unsigned char session_key[16]; uint32_t reserved[16]; }; // the kernel accepts a response only at exactly its struct's size static_assert(sizeof(StartupRequestMsg) == 744, "ksmbd_startup_request"); static_assert(sizeof(LoginRequestMsg) == 116, "ksmbd_login_request"); Loading @@ -216,6 +237,8 @@ static_assert(sizeof(TreeConnectRequestMsg) == 264, "ksmbd_tree_connect_request" static_assert(sizeof(TreeConnectResponseMsg) == 72, "ksmbd_tree_connect_response"); static_assert(sizeof(RpcCommandMsg) == 12, "ksmbd_rpc_command"); static_assert(sizeof(SpnegoAuthenResponseMsg) == 168, "ksmbd_spnego_authen_response"); static_assert(sizeof(NtlmAuthenRequestMsg) == 88, "ksmbd_ntlm_authen_request"); static_assert(sizeof(NtlmAuthenResponseMsg) == 88, "ksmbd_ntlm_authen_response"); enum : uint16_t { UserOk = 1 << 0, Loading Loading @@ -650,6 +673,7 @@ void SmbServer::handle(int cmd, const unsigned char *payload, size_t len) { case TreeConnectRequest: treeConnect(payload, len); break; case RpcRequest: rpc(payload, len); break; case SpnegoAuthenRequest: spnego(payload, len); break; case NtlmAuthenRequest: ntlmAuthen(payload, len); break; default: break; // heartbeat, tree disconnect, logout: nothing to answer } } Loading Loading @@ -681,6 +705,18 @@ void SmbServer::login(const unsigned char *payload, size_t len) { if (acct.idR || groupsOfR(acct).size() > 1) resp.status |= UserExtension; #else if (groupsOf(acct.name.c_str(), acct.gid).size() > 1) resp.status |= UserExtension; #endif copy(resp.account, sizeof(resp.account), acct.name); } else if (found && !root && !locked(acct.name.c_str()) && adconnectPresent()) { // an AD user has no local NT hash: tell ksmbd to pass the NTLM // response through to the domain controller (NTLM_AUTHEN_REQUEST) resp.uid = acct.uid; resp.gid = acct.gid; resp.status = UserOk | UserPassthrough; #ifdef SHAREDD_UID_R if (acct.idR || groupsOfR(acct).size() > 1) resp.status |= UserExtension; #else if (groupsOf(acct.name.c_str(), acct.gid).size() > 1) resp.status |= UserExtension; #endif copy(resp.account, sizeof(resp.account), acct.name); } else if (!found && _cfg.settings.mapToGuest == "bad_user") { Loading Loading @@ -938,6 +974,79 @@ bool askAdconnect(const unsigned char *blob, size_t len, KrbLogin &out, std::str return true; } bool adconnectPresent() { struct stat st; return stat(SHAREDD_ADCONNECT_SOCKET, &st) == 0 && S_ISSOCK(st.st_mode); } // a UTF-16LE name from the kernel as an 8-bit string (ASCII/Latin-1: the low // byte of each unit -- AD account and domain names are effectively ASCII) std::string utf16leLow(const unsigned char *p, size_t bytes) { std::string out; for (size_t i = 0; i + 1 < bytes; i += 2) out += (char)p[i]; return out; } // have adconnectd check a domain user's NTLM response at the DC (NETLOGON). // true with the 16-byte base session key on success; false if the password // was refused or adconnectd couldn't be reached (*error set). bool ntlmViaAdconnect(const std::string &domain, const std::string &account, const std::string &workstation, const unsigned char challenge[8], const std::string &nt, const std::string &lm, std::string &sessionKey, std::string &error) { int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); if (fd < 0) { error = strerror(errno); return false; } sockaddr_un a{}; a.sun_family = AF_UNIX; snprintf(a.sun_path, sizeof a.sun_path, "%s", SHAREDD_ADCONNECT_SOCKET); struct timeval tv = {30, 0}; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); if (connect(fd, (sockaddr *)&a, sizeof a) != 0) { error = std::string("adconnectd isn't running (") + strerror(errno) + ")"; close(fd); return false; } /* six uint16-length-prefixed fields: domain, account, workstation, * server challenge, NT response, LM response */ std::string payload; auto fld = [&](const std::string &s) { payload += (char)(s.size() & 0xff); payload += (char)((s.size() >> 8) & 0xff); payload += s; }; fld(domain); fld(account); fld(workstation); fld(std::string((const char *)challenge, 8)); fld(nt); fld(lm); std::string req = "NTLM " + base64((const unsigned char *)payload.data(), payload.size()) + "\n", reply; bool ok = ::send(fd, req.data(), req.size(), MSG_NOSIGNAL) == (ssize_t)req.size(); char buf[8192]; for (ssize_t n; ok && (n = recv(fd, buf, sizeof buf, 0)) > 0;) reply.append(buf, (size_t)n); close(fd); if (reply.compare(0, 4, "ERR ") == 0) { error = reply.substr(4, reply.find('\n') - 4); return false; } /* N <status> <base64 session key> */ size_t nl = reply.find('\n'); if (reply.compare(0, 2, "N\t") != 0 || nl == std::string::npos) { error = "bad answer from adconnectd"; return false; } std::vector<std::string> f; std::string line = reply.substr(2, nl - 2); size_t p = 0; for (size_t q; (q = line.find('\t', p)) != std::string::npos; p = q + 1) f.push_back(line.substr(p, q - p)); f.push_back(line.substr(p)); if (f.size() != 2) { error = "bad answer from adconnectd"; return false; } unsigned long status = strtoul(f[0].c_str(), nullptr, 10); if (status != 0) { error = "the domain controller refused the password"; return false; } if (!unbase64(f[1], sessionKey)) { error = "bad answer from adconnectd"; return false; } return true; } } // namespace // Kerberos logins (domain members asking for cifs/<this system>): the Loading Loading @@ -997,4 +1106,44 @@ void SmbServer::spnego(const unsigned char *payload, size_t len) { send(SpnegoAuthenResponse, msg.data(), msg.size(), false); } // NTLM logins of domain users (no Kerberos): ksmbd forwards the client's // response here for an account marked UserPassthrough; adconnectd has its // domain controller check it over NETLOGON and returns the session key. void SmbServer::ntlmAuthen(const unsigned char *payload, size_t len) { NtlmAuthenResponseMsg resp; memset(&resp, 0, sizeof(resp)); resp.status = UserBadPassword; if (len < sizeof(NtlmAuthenRequestMsg)) { send(NtlmAuthenResponse, &resp, sizeof(resp), false); return; } NtlmAuthenRequestMsg req; memcpy(&req, payload, sizeof(req)); resp.handle = req.handle; size_t off = sizeof(NtlmAuthenRequestMsg); size_t need = (size_t)req.user_name_len + req.domain_name_len + req.workstation_len + req.nt_response_len + req.lm_response_len; if (off + need > len) { send(NtlmAuthenResponse, &resp, sizeof(resp), false); return; } const unsigned char *p = payload + off; std::string user = utf16leLow(p, req.user_name_len); p += req.user_name_len; std::string domain = utf16leLow(p, req.domain_name_len); p += req.domain_name_len; std::string ws = utf16leLow(p, req.workstation_len); p += req.workstation_len; std::string nt((const char *)p, req.nt_response_len); p += req.nt_response_len; std::string lm((const char *)p, req.lm_response_len); p += req.lm_response_len; std::string key, error; if (ntlmViaAdconnect(domain, user, ws, req.challenge, nt, lm, key, error)) { resp.status = UserOk; memcpy(resp.session_key, key.data(), std::min<size_t>(key.size(), sizeof(resp.session_key))); std::fill(key.begin(), key.end(), '\0'); } else { std::cerr << "sharedd: SMB: NTLM login for " << domain << "\\" << user << " refused: " << error << std::endl; } send(NtlmAuthenResponse, &resp, sizeof(resp), false); } } // namespace sharedd