Loading plugin/ad/ad.cpp +151 −3 Changes for plugin/ad/ad.cpp: 151 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ #include "ad.h" #include "crypto/gss.h" #include "crypto/ntlm.h" #include <sys/socket.h> #include <netinet/in.h> Loading Loading @@ -506,6 +507,45 @@ void ActiveDirectory::RequestEvent(netplus::con &curcon, const int, ULONG_PTR) { namespace gss = fenrirds::crypto::gss; /* NTLM2 sign+seal layer: each message is a 4-byte-length-prefixed NTLM * signature + sealed LDAP PDU; responses are sealed the same way. */ if (session->ntlmSec && session->ntlmSec->valid) { size_t consumed = 0; while (true) { size_t avail = curcon.RecvData.size() - consumed; if (avail < 4) break; const uint8_t *p = reinterpret_cast<const uint8_t*>(curcon.RecvData.data()) + consumed; uint32_t blen = uint32_t(p[0]) << 24 | uint32_t(p[1]) << 16 | uint32_t(p[2]) << 8 | p[3]; if (avail - 4 < blen) break; std::vector<uint8_t> plain; if (!session->ntlmSec->unseal(p, 4 + blen, plain)) { std::cerr << "[LDAP] NTLM unseal FAILED -> dropping connection" << std::endl; curcon.RecvData.clear(); return; } consumed += 4 + blen; /* process the LDAP PDU(s) inside the unsealed buffer */ std::vector<uint8_t> responses; size_t po = 0; while (po < plain.size()) { size_t o = po; uint8_t t; const uint8_t *mc; size_t ml; if (!LdapBer::parseTLV(plain.data(), plain.size(), o, t, mc, ml) || t != LdapBer::TAG_SEQUENCE) break; auto r = handleMessage(mc, ml, *session); responses.insert(responses.end(), r.begin(), r.end()); po = o; } if (!responses.empty()) { auto sealed = session->ntlmSec->seal(responses); curcon.SendData.insert(curcon.SendData.end(), sealed.begin(), sealed.end()); } } if (consumed > 0) curcon.RecvData.erase(curcon.RecvData.begin(), curcon.RecvData.begin() + consumed); return; } /* With a SASL security layer, each message is a 4-byte-length-prefixed GSS * token wrapping the LDAP PDU; responses are wrapped the same way. */ if (session->ssf > 0) { Loading Loading @@ -569,8 +609,19 @@ void ActiveDirectory::RequestEvent(netplus::con &curcon, const int, ULONG_PTR) { if (!LdapBer::parseTLV(raw, rawLen, off, outerTag, msgContent, msgLen)) break; /* incomplete PDU: wait for more */ if (outerTag != LdapBer::TAG_SEQUENCE) break; if (outerTag != LdapBer::TAG_SEQUENCE) { /* not an LDAP message (e.g. a client-side signed/sealed SASL token we * did not negotiate a security layer for): drop the whole buffer so * we do not re-process it forever (CPU spin). */ { std::string hx; static const char *hc="0123456789abcdef"; for (size_t i=0;i<rawLen && i<56;i++){ hx+=hc[raw[i]>>4]; hx+=hc[raw[i]&0xf]; } std::cerr << "[LDAP] non-SEQUENCE (" << rawLen << "B, authed=" << session->authenticated << "): " << hx << std::endl; } curcon.RecvData.clear(); return; } std::vector<uint8_t> response = handleMessage(msgContent, msgLen, *session); if (!response.empty()) Loading Loading @@ -701,8 +752,12 @@ std::vector<uint8_t> ActiveDirectory::handleBindRequest(const uint8_t *content, namespace gss = fenrirds::crypto::gss; std::string mech = toLower(mechanism); bool spnego = (mech == "gss-spnego"); { std::string hex; static const char *hc="0123456789abcdef"; for (size_t i=0;i<inTok.size() && i<48;i++){ hex+=hc[inTok[i]>>4]; hex+=hc[inTok[i]&0xf]; } std::cerr << "[LDAP] bind SASL mech='" << mechanism << "' gssState=" << int(session.gssState) << " inTok=" << inTok.size() << "B" << std::endl; << int(session.gssState) << " inTok=" << inTok.size() << "B: " << hex << std::endl; } if (mech != "gssapi" && !spnego) { session.gssState = LdapSession::GssNone; std::cerr << "[LDAP] bind SASL -> AUTH_METHOD_NOT_SUPPORTED (mech)" << std::endl; Loading @@ -711,6 +766,76 @@ std::vector<uint8_t> ActiveDirectory::handleBindRequest(const uint8_t *content, if (!g_KdcPlugin) return bindResp(LDAP_AUTH_METHOD_NOT_SUPPORTED, nullptr); /* ---- NTLMSSP over GSS-SPNEGO (not-yet-domain-joined client) ---- */ { namespace ntlm = fenrirds::crypto::ntlm; static const uint8_t NTLMSSP_OID[] = {0x06,0x0a,0x2b,0x06,0x01,0x04,0x01,0x82,0x37,0x02,0x02,0x0a}; std::vector<uint8_t> mechTok; bool rawNtlm = false; if (ntlm::isNtlmssp(inTok)) { mechTok = inTok; /* raw NTLMSSP, no SPNEGO wrapping */ rawNtlm = true; } else if (spnego) { std::vector<uint8_t> mt, mic; int ns = 0; if (gss::parseSpnegoInit(inTok, mt, session.gssMechTypes)) mechTok = mt; else if (gss::parseSpnegoResp(inTok, mt, mic, ns)) mechTok = mt; } else { uint16_t id = 0; std::vector<uint8_t> x; mechTok = gss::parseInitialToken(inTok, id, x) ? x : inTok; } bool wrapSpnego = spnego && !rawNtlm; if (ntlm::isNtlmssp(mechTok)) { uint32_t ty = ntlm::messageType(mechTok); if (ty == 1) { session.ntlmChallenge = crypto::randomBytes(8); session.ntlmInProgress = true; std::string dnsHost = _DcHost.empty() ? std::string("dc") : _DcHost; std::string dnsDomain = dnsHost.find('.') != std::string::npos ? dnsHost.substr(dnsHost.find('.')+1) : std::string(); std::string nbHost = dsdb::upper(dnsHost.substr(0, dnsHost.find('.'))); std::string nbDomain = dsdb::upper(dnsDomain.substr(0, dnsDomain.find('.'))); auto type2 = ntlm::buildChallenge(session.ntlmChallenge, nbDomain, nbHost, dnsDomain, dnsHost); std::cerr << "[LDAP] bind NTLMSSP NEGOTIATE -> CHALLENGE (spnego=" << wrapSpnego << ")" << std::endl; std::vector<uint8_t> creds; if (wrapSpnego) { std::vector<uint8_t> oid(NTLMSSP_OID, NTLMSSP_OID + sizeof(NTLMSSP_OID)); creds = gss::buildSpnegoResp(gss::ACCEPT_INCOMPLETE, &oid, type2, {}); } else creds = type2; return bindResp(SASL_BIND_IN_PROGRESS, &creds); } if (ty == 3) { ntlm::Auth3 a; if (!ntlm::parseAuth3(mechTok, a)) return bindResp(LDAP_INVALID_CREDENTIALS, nullptr); std::vector<uint8_t> ntHash; std::string princ; if (!g_KdcPlugin->ntlmNtHash(a.user, a.domain, ntHash, princ)) { std::cerr << "[LDAP] bind NTLMSSP '" << a.domain << "\\" << a.user << "' -> UNKNOWN USER" << std::endl; return bindResp(LDAP_INVALID_CREDENTIALS, nullptr); } std::vector<uint8_t> exported; if (!ntlm::verifyV2(ntHash, a, session.ntlmChallenge, exported)) { std::cerr << "[LDAP] bind NTLMSSP '" << a.domain << "\\" << a.user << "' -> BAD PASSWORD" << std::endl; return bindResp(LDAP_INVALID_CREDENTIALS, nullptr); } std::cerr << "[LDAP] bind NTLMSSP '" << a.domain << "\\" << a.user << "' -> SUCCESS (" << princ << ")" << std::endl; session.authenticated = true; session.bindDN = princ; session.gssClient = princ; session.ntlmInProgress = false; /* the client seals subsequent PDUs -> stand up the NTLM2 * sign+seal layer keyed on the exported session key */ session.ntlmSec = std::make_shared<ntlm::Security>(ntlm::makeSecurity(exported)); std::vector<uint8_t> creds; if (wrapSpnego) creds = gss::buildSpnegoResp(gss::ACCEPT_COMPLETED, nullptr, {}, {}); return bindResp(LDAP_SUCCESS, creds.empty() ? nullptr : &creds); } } } if (session.gssState == LdapSession::GssNone) { std::vector<uint8_t> apReq; session.gssSpnego = spnego; Loading Loading @@ -902,6 +1027,15 @@ std::vector<uint8_t> ActiveDirectory::handleSearchRequest(const uint8_t *content } } { std::string attrs; for (auto &a : requested) { if (!attrs.empty()) attrs += ","; attrs += a; } std::string fhex; static const char *hc="0123456789abcdef"; for (size_t i=filterOff;i<off && i<filterOff+48;i++){ fhex+=hc[content[i]>>4]; fhex+=hc[content[i]&0xf]; } std::cerr << "[LDAP] SEARCH msgId=" << msgId << " base='" << baseObject << "' scope=" << scopeVal << " attrs=[" << attrs << "] filter=" << fhex << std::endl; } std::vector<uint8_t> out; int32_t count = 0; dsdb::MatchContext rootCtx; Loading Loading @@ -945,6 +1079,8 @@ std::vector<uint8_t> ActiveDirectory::handleSearchRequest(const uint8_t *content }); } (void)found; std::cerr << "[LDAP] SEARCH msgId=" << msgId << " -> " << count << " entries, rc=" << int(rc) << (found ? "" : " (no matching NC)") << std::endl; auto done = buildSearchResultDone(msgId, rc); out.insert(out.end(), done.begin(), done.end()); return out; Loading Loading @@ -997,6 +1133,18 @@ LdapEntry ActiveDirectory::renderEntry(const dsdb::Entry &ent, a.values = vals; le.attributes.push_back(std::move(a)); } /* objectGUID is the record UUID (a separate Entry field, not in attrs): * synthesize it as the 16-byte binary GUID AD clients expect. Windows * reads the domain/computer objectGUID during a join and rejects an entry * that lacks it (ERROR_INVALID_PARAMETER). */ if (!ent.attrs.count("objectGUID") && wanted("objectGUID", findAttribute("objectGUID"))) { LdapAttribute a; a.type = "objectGUID"; if (!typesOnly) a.values.push_back(std::string(reinterpret_cast<const char*>(ent.objectGuid.value), 16)); le.attributes.push_back(std::move(a)); } return le; } Loading plugin/ad/ad.h +6 −0 Changes for plugin/ad/ad.h: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -43,6 +43,7 @@ #include "backend.h" #include "plugin.h" #include "crypto/ntlm.h" #include "dsdb/directory.h" #include "dsdb/schema.h" Loading Loading @@ -217,6 +218,11 @@ namespace fenrirds { bool gssSpnego = false; std::vector<uint8_t> gssMechTypes; /* DER of the client's MechTypeList (for the MIC) */ /* NTLMSSP (GSS-SPNEGO with NTLM) bind state */ bool ntlmInProgress = false; std::vector<uint8_t> ntlmChallenge; /* 8-byte server challenge */ std::shared_ptr<fenrirds::crypto::ntlm::Security> ntlmSec; /* sign+seal layer */ /* established SASL security layer (after a successful GSSAPI bind): * ssf 0 = none, 1 = integrity (sign), 2 = confidentiality (seal) */ int ssf = 0; Loading plugin/krb5/krb5.cpp +34 −0 Changes for plugin/krb5/krb5.cpp: 34 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -3643,6 +3643,40 @@ std::vector<uint8_t> &Krb5KDC::realmTgsKey(RealmInfo &ri) { return ri.tgsKey; } bool Krb5KDC::ntlmNtHash(const std::string &user, const std::string &domain, std::vector<uint8_t> &ntHash, std::string &principal) { std::lock_guard<std::mutex> guard(_Lock); RealmInfo *ri = nullptr; if (!domain.empty()) { std::string du = dsdb::upper(domain); for (auto &kv : _Realms) { if (!kv.second.dir) continue; if (dsdb::upper(kv.second.meta.netbios) == du || dsdb::upper(kv.second.meta.dnsName) == du || dsdb::upper(kv.second.meta.realm) == du) { ri = &kv.second; break; } } } if (!ri) for (auto &kv : _Realms) if (kv.second.dir) { ri = &kv.second; break; } if (!ri || !ri->dir) return false; std::optional<dsdb::Entry> e = ri->dir->bySamAccountName(user); if (!e && user.find('@') != std::string::npos) e = ri->dir->byUpn(user); if (!e && !user.empty() && user.back() != '$') e = ri->dir->bySamAccountName(user + "$"); if (!e) return false; dsdb::Secrets sec = ri->dir->secretsOf(e->objectGuid); if (sec.ntHash.size() != 16) return false; ntHash.assign(sec.ntHash.begin(), sec.ntHash.end()); principal = user + "@" + ri->meta.realm; return true; } bool Krb5KDC::gssAccept(const std::vector<uint8_t> &apReqData, GssAcceptResult &out) { std::lock_guard<std::mutex> guard(_Lock); Loading plugin/krb5/krb5.h +2 −0 Changes for plugin/krb5/krb5.h: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -732,6 +732,8 @@ namespace fenrirds { int etype) override; bool gssAccept(const std::vector<uint8_t> &apReq, GssAcceptResult &out) override; bool ntlmNtHash(const std::string &user, const std::string &domain, std::vector<uint8_t> &ntHash, std::string &principal) override; /* (Re-)load realms from the admin backend (called in ctor) */ void loadRealms(); Loading plugin/smb/smb.cpp +82 −3 Changes for plugin/smb/smb.cpp: 82 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -7,6 +7,8 @@ #include "crypto/crypto.h" #include "crypto/gss.h" #include "crypto/ntlm.h" #include "dsdb/dn.h" #include "rpc/ndr.h" #include "rpc/services.h" #include "smb.h" Loading @@ -22,6 +24,9 @@ namespace { void w64(std::vector<uint8_t>&o,uint64_t v){for(int i=0;i<8;i++)o.push_back((v>>(8*i))&0xff);} constexpr uint32_t FSCTL_PIPE_TRANSCEIVE = 0x0011C017; /* NTLMSSP mech OID 1.3.6.1.4.1.311.2.2.10 (DER-encoded OID value). */ const uint8_t NTLMSSP_OID[] = {0x06,0x0a,0x2b,0x06,0x01,0x04,0x01,0x82,0x37,0x02,0x02,0x0a}; } /***************************************************************************** Loading Loading @@ -88,9 +93,16 @@ std::vector<uint8_t> RpcPipe::dispatch(const rpc::RequestPdu &req) { res.fault = true; res.faultCode = 0x1C010003; /* nca_unk_if */ } { static const char *hc = "0123456789abcdef"; std::string rq, rs; for (size_t i=0;i<req.stub.size() && i<64;i++){ rq+=hc[req.stub[i]>>4]; rq+=hc[req.stub[i]&0xf]; } for (size_t i=0;i<res.stub.size() && i<96;i++){ rs+=hc[res.stub[i]>>4]; rs+=hc[res.stub[i]&0xf]; } std::cerr << "[RPC] " << iface << " opnum=" << req.opnum << " stub=" << req.stub.size() << "B -> " << (res.fault ? "FAULT 0x" : "OK status=") << std::hex << (res.fault ? res.faultCode : res.status) << std::dec << std::endl; << std::hex << (res.fault ? res.faultCode : res.status) << std::dec << "\n req: " << rq << "\n res: " << rs << std::endl; } if (res.fault) return rpc::buildFault(req.callId, req.contextId, res.faultCode); return rpc::buildResponse(req.callId, req.contextId, res.stub); Loading Loading @@ -304,15 +316,82 @@ std::vector<uint8_t> SmbServer::handleMessage(Conn &c, const std::vector<uint8_t std::vector<uint8_t> apReq, mechTypes; bool spnego = false; std::vector<uint8_t> mt; std::vector<uint8_t> mt, mic; int nstate = 0; if (gss::parseSpnegoInit(tok, mt, mechTypes)) { spnego = true; apReq.swap(mt); } else if (gss::parseSpnegoResp(tok, mt, mic, nstate)) { spnego = true; apReq.swap(mt); } else { uint16_t id=0; gss::parseInitialToken(tok, id, apReq); } if (!g_KdcPlugin) return errorResponse(req, smb::STATUS_NOT_SUPPORTED); /* the AP-REQ inside (strip the krb5 GSS framing if present) */ /* strip any krb5 GSS framing (Kerberos AP-REQ is wrapped, NTLM is not) */ std::vector<uint8_t> inner = apReq; { uint16_t id=0; std::vector<uint8_t> x; if (gss::parseInitialToken(apReq, id, x)) inner.swap(x); } /* ---- NTLMSSP path (not-yet-domain-joined client) ---- */ namespace ntlm = crypto::ntlm; const std::vector<uint8_t> &ntok = ntlm::isNtlmssp(apReq) ? apReq : inner; if (ntlm::isNtlmssp(ntok)) { if (!c.sessionId) { auto r = crypto::randomBytes(8); c.sessionId = (rd64(r.data()) | 1); } uint32_t ty = ntlm::messageType(ntok); if (ty == 1) { c.ntlmChallenge = crypto::randomBytes(8); std::string dnsHost = _DcHost.empty() ? std::string("dc") : _DcHost; std::string dnsDomain = dnsHost.find('.') != std::string::npos ? dnsHost.substr(dnsHost.find('.')+1) : std::string(); std::string nbHost = dsdb::upper(dnsHost.substr(0, dnsHost.find('.'))); std::string nbDomain = dsdb::upper(dnsDomain.substr(0, dnsDomain.find('.'))); auto type2 = ntlm::buildChallenge(c.ntlmChallenge, nbDomain, nbHost, dnsDomain, dnsHost); std::vector<uint8_t> oid(NTLMSSP_OID, NTLMSSP_OID + sizeof(NTLMSSP_OID)); std::vector<uint8_t> outTok = spnego ? gss::buildSpnegoResp(gss::ACCEPT_INCOMPLETE, &oid, type2, {}) : type2; std::cerr << "[SMB] NTLMSSP NEGOTIATE -> CHALLENGE" << std::endl; auto resp = respHeader(smb::STATUS_MORE_PROCESSING); std::vector<uint8_t> b2; w16(b2, 9); w16(b2, 0); w16(b2, 72); w16(b2, uint16_t(outTok.size())); b2.insert(b2.end(), outTok.begin(), outTok.end()); resp.insert(resp.end(), b2.begin(), b2.end()); if (c.dialect >= smb::SMB_3_1_1) c.preauth = smb::preauthUpdate(c.preauth, resp.data(), resp.size()); return resp; /* unsigned: no session keys yet */ } if (ty == 3) { ntlm::Auth3 a; if (!ntlm::parseAuth3(ntok, a)) return errorResponse(req, smb::STATUS_LOGON_FAILURE); std::vector<uint8_t> ntHash; std::string princ; if (!g_KdcPlugin->ntlmNtHash(a.user, a.domain, ntHash, princ)) { std::cerr << "[SMB] NTLMSSP AUTH '" << a.domain << "\\" << a.user << "' -> UNKNOWN USER" << std::endl; return errorResponse(req, smb::STATUS_LOGON_FAILURE); } std::vector<uint8_t> exported; if (!ntlm::verifyV2(ntHash, a, c.ntlmChallenge, exported)) { std::cerr << "[SMB] NTLMSSP AUTH '" << a.domain << "\\" << a.user << "' -> BAD PASSWORD" << std::endl; return errorResponse(req, smb::STATUS_LOGON_FAILURE); } std::cerr << "[SMB] NTLMSSP AUTH '" << a.domain << "\\" << a.user << "' -> SUCCESS (" << princ << ")" << std::endl; c.authenticated = true; c.clientPrincipal = princ; c.sessionKey = exported; std::vector<uint8_t> outTok = spnego ? gss::buildSpnegoResp(gss::ACCEPT_COMPLETED, nullptr, {}, {}) : std::vector<uint8_t>(); auto resp = respHeader(smb::STATUS_SUCCESS); std::vector<uint8_t> b2; w16(b2, 9); w16(b2, 0); w16(b2, 72); w16(b2, uint16_t(outTok.size())); b2.insert(b2.end(), outTok.begin(), outTok.end()); resp.insert(resp.end(), b2.begin(), b2.end()); c.keys = smb::deriveKeys(c.dialect, exported, c.dialect >= smb::SMB_3_1_1 ? c.preauth : std::vector<uint8_t>()); c.haveKeys = true; return finish(resp); } return errorResponse(req, smb::STATUS_LOGON_FAILURE); } /* ---- Kerberos path ---- */ GssAcceptResult res; if (!g_KdcPlugin->gssAccept(inner, res)) { std::cerr << "[SMB] SESSION_SETUP gssAccept FAILED (apReq=" << inner.size() Loading Loading
plugin/ad/ad.cpp +151 −3 Changes for plugin/ad/ad.cpp: 151 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ #include "ad.h" #include "crypto/gss.h" #include "crypto/ntlm.h" #include <sys/socket.h> #include <netinet/in.h> Loading Loading @@ -506,6 +507,45 @@ void ActiveDirectory::RequestEvent(netplus::con &curcon, const int, ULONG_PTR) { namespace gss = fenrirds::crypto::gss; /* NTLM2 sign+seal layer: each message is a 4-byte-length-prefixed NTLM * signature + sealed LDAP PDU; responses are sealed the same way. */ if (session->ntlmSec && session->ntlmSec->valid) { size_t consumed = 0; while (true) { size_t avail = curcon.RecvData.size() - consumed; if (avail < 4) break; const uint8_t *p = reinterpret_cast<const uint8_t*>(curcon.RecvData.data()) + consumed; uint32_t blen = uint32_t(p[0]) << 24 | uint32_t(p[1]) << 16 | uint32_t(p[2]) << 8 | p[3]; if (avail - 4 < blen) break; std::vector<uint8_t> plain; if (!session->ntlmSec->unseal(p, 4 + blen, plain)) { std::cerr << "[LDAP] NTLM unseal FAILED -> dropping connection" << std::endl; curcon.RecvData.clear(); return; } consumed += 4 + blen; /* process the LDAP PDU(s) inside the unsealed buffer */ std::vector<uint8_t> responses; size_t po = 0; while (po < plain.size()) { size_t o = po; uint8_t t; const uint8_t *mc; size_t ml; if (!LdapBer::parseTLV(plain.data(), plain.size(), o, t, mc, ml) || t != LdapBer::TAG_SEQUENCE) break; auto r = handleMessage(mc, ml, *session); responses.insert(responses.end(), r.begin(), r.end()); po = o; } if (!responses.empty()) { auto sealed = session->ntlmSec->seal(responses); curcon.SendData.insert(curcon.SendData.end(), sealed.begin(), sealed.end()); } } if (consumed > 0) curcon.RecvData.erase(curcon.RecvData.begin(), curcon.RecvData.begin() + consumed); return; } /* With a SASL security layer, each message is a 4-byte-length-prefixed GSS * token wrapping the LDAP PDU; responses are wrapped the same way. */ if (session->ssf > 0) { Loading Loading @@ -569,8 +609,19 @@ void ActiveDirectory::RequestEvent(netplus::con &curcon, const int, ULONG_PTR) { if (!LdapBer::parseTLV(raw, rawLen, off, outerTag, msgContent, msgLen)) break; /* incomplete PDU: wait for more */ if (outerTag != LdapBer::TAG_SEQUENCE) break; if (outerTag != LdapBer::TAG_SEQUENCE) { /* not an LDAP message (e.g. a client-side signed/sealed SASL token we * did not negotiate a security layer for): drop the whole buffer so * we do not re-process it forever (CPU spin). */ { std::string hx; static const char *hc="0123456789abcdef"; for (size_t i=0;i<rawLen && i<56;i++){ hx+=hc[raw[i]>>4]; hx+=hc[raw[i]&0xf]; } std::cerr << "[LDAP] non-SEQUENCE (" << rawLen << "B, authed=" << session->authenticated << "): " << hx << std::endl; } curcon.RecvData.clear(); return; } std::vector<uint8_t> response = handleMessage(msgContent, msgLen, *session); if (!response.empty()) Loading Loading @@ -701,8 +752,12 @@ std::vector<uint8_t> ActiveDirectory::handleBindRequest(const uint8_t *content, namespace gss = fenrirds::crypto::gss; std::string mech = toLower(mechanism); bool spnego = (mech == "gss-spnego"); { std::string hex; static const char *hc="0123456789abcdef"; for (size_t i=0;i<inTok.size() && i<48;i++){ hex+=hc[inTok[i]>>4]; hex+=hc[inTok[i]&0xf]; } std::cerr << "[LDAP] bind SASL mech='" << mechanism << "' gssState=" << int(session.gssState) << " inTok=" << inTok.size() << "B" << std::endl; << int(session.gssState) << " inTok=" << inTok.size() << "B: " << hex << std::endl; } if (mech != "gssapi" && !spnego) { session.gssState = LdapSession::GssNone; std::cerr << "[LDAP] bind SASL -> AUTH_METHOD_NOT_SUPPORTED (mech)" << std::endl; Loading @@ -711,6 +766,76 @@ std::vector<uint8_t> ActiveDirectory::handleBindRequest(const uint8_t *content, if (!g_KdcPlugin) return bindResp(LDAP_AUTH_METHOD_NOT_SUPPORTED, nullptr); /* ---- NTLMSSP over GSS-SPNEGO (not-yet-domain-joined client) ---- */ { namespace ntlm = fenrirds::crypto::ntlm; static const uint8_t NTLMSSP_OID[] = {0x06,0x0a,0x2b,0x06,0x01,0x04,0x01,0x82,0x37,0x02,0x02,0x0a}; std::vector<uint8_t> mechTok; bool rawNtlm = false; if (ntlm::isNtlmssp(inTok)) { mechTok = inTok; /* raw NTLMSSP, no SPNEGO wrapping */ rawNtlm = true; } else if (spnego) { std::vector<uint8_t> mt, mic; int ns = 0; if (gss::parseSpnegoInit(inTok, mt, session.gssMechTypes)) mechTok = mt; else if (gss::parseSpnegoResp(inTok, mt, mic, ns)) mechTok = mt; } else { uint16_t id = 0; std::vector<uint8_t> x; mechTok = gss::parseInitialToken(inTok, id, x) ? x : inTok; } bool wrapSpnego = spnego && !rawNtlm; if (ntlm::isNtlmssp(mechTok)) { uint32_t ty = ntlm::messageType(mechTok); if (ty == 1) { session.ntlmChallenge = crypto::randomBytes(8); session.ntlmInProgress = true; std::string dnsHost = _DcHost.empty() ? std::string("dc") : _DcHost; std::string dnsDomain = dnsHost.find('.') != std::string::npos ? dnsHost.substr(dnsHost.find('.')+1) : std::string(); std::string nbHost = dsdb::upper(dnsHost.substr(0, dnsHost.find('.'))); std::string nbDomain = dsdb::upper(dnsDomain.substr(0, dnsDomain.find('.'))); auto type2 = ntlm::buildChallenge(session.ntlmChallenge, nbDomain, nbHost, dnsDomain, dnsHost); std::cerr << "[LDAP] bind NTLMSSP NEGOTIATE -> CHALLENGE (spnego=" << wrapSpnego << ")" << std::endl; std::vector<uint8_t> creds; if (wrapSpnego) { std::vector<uint8_t> oid(NTLMSSP_OID, NTLMSSP_OID + sizeof(NTLMSSP_OID)); creds = gss::buildSpnegoResp(gss::ACCEPT_INCOMPLETE, &oid, type2, {}); } else creds = type2; return bindResp(SASL_BIND_IN_PROGRESS, &creds); } if (ty == 3) { ntlm::Auth3 a; if (!ntlm::parseAuth3(mechTok, a)) return bindResp(LDAP_INVALID_CREDENTIALS, nullptr); std::vector<uint8_t> ntHash; std::string princ; if (!g_KdcPlugin->ntlmNtHash(a.user, a.domain, ntHash, princ)) { std::cerr << "[LDAP] bind NTLMSSP '" << a.domain << "\\" << a.user << "' -> UNKNOWN USER" << std::endl; return bindResp(LDAP_INVALID_CREDENTIALS, nullptr); } std::vector<uint8_t> exported; if (!ntlm::verifyV2(ntHash, a, session.ntlmChallenge, exported)) { std::cerr << "[LDAP] bind NTLMSSP '" << a.domain << "\\" << a.user << "' -> BAD PASSWORD" << std::endl; return bindResp(LDAP_INVALID_CREDENTIALS, nullptr); } std::cerr << "[LDAP] bind NTLMSSP '" << a.domain << "\\" << a.user << "' -> SUCCESS (" << princ << ")" << std::endl; session.authenticated = true; session.bindDN = princ; session.gssClient = princ; session.ntlmInProgress = false; /* the client seals subsequent PDUs -> stand up the NTLM2 * sign+seal layer keyed on the exported session key */ session.ntlmSec = std::make_shared<ntlm::Security>(ntlm::makeSecurity(exported)); std::vector<uint8_t> creds; if (wrapSpnego) creds = gss::buildSpnegoResp(gss::ACCEPT_COMPLETED, nullptr, {}, {}); return bindResp(LDAP_SUCCESS, creds.empty() ? nullptr : &creds); } } } if (session.gssState == LdapSession::GssNone) { std::vector<uint8_t> apReq; session.gssSpnego = spnego; Loading Loading @@ -902,6 +1027,15 @@ std::vector<uint8_t> ActiveDirectory::handleSearchRequest(const uint8_t *content } } { std::string attrs; for (auto &a : requested) { if (!attrs.empty()) attrs += ","; attrs += a; } std::string fhex; static const char *hc="0123456789abcdef"; for (size_t i=filterOff;i<off && i<filterOff+48;i++){ fhex+=hc[content[i]>>4]; fhex+=hc[content[i]&0xf]; } std::cerr << "[LDAP] SEARCH msgId=" << msgId << " base='" << baseObject << "' scope=" << scopeVal << " attrs=[" << attrs << "] filter=" << fhex << std::endl; } std::vector<uint8_t> out; int32_t count = 0; dsdb::MatchContext rootCtx; Loading Loading @@ -945,6 +1079,8 @@ std::vector<uint8_t> ActiveDirectory::handleSearchRequest(const uint8_t *content }); } (void)found; std::cerr << "[LDAP] SEARCH msgId=" << msgId << " -> " << count << " entries, rc=" << int(rc) << (found ? "" : " (no matching NC)") << std::endl; auto done = buildSearchResultDone(msgId, rc); out.insert(out.end(), done.begin(), done.end()); return out; Loading Loading @@ -997,6 +1133,18 @@ LdapEntry ActiveDirectory::renderEntry(const dsdb::Entry &ent, a.values = vals; le.attributes.push_back(std::move(a)); } /* objectGUID is the record UUID (a separate Entry field, not in attrs): * synthesize it as the 16-byte binary GUID AD clients expect. Windows * reads the domain/computer objectGUID during a join and rejects an entry * that lacks it (ERROR_INVALID_PARAMETER). */ if (!ent.attrs.count("objectGUID") && wanted("objectGUID", findAttribute("objectGUID"))) { LdapAttribute a; a.type = "objectGUID"; if (!typesOnly) a.values.push_back(std::string(reinterpret_cast<const char*>(ent.objectGuid.value), 16)); le.attributes.push_back(std::move(a)); } return le; } Loading
plugin/ad/ad.h +6 −0 Changes for plugin/ad/ad.h: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -43,6 +43,7 @@ #include "backend.h" #include "plugin.h" #include "crypto/ntlm.h" #include "dsdb/directory.h" #include "dsdb/schema.h" Loading Loading @@ -217,6 +218,11 @@ namespace fenrirds { bool gssSpnego = false; std::vector<uint8_t> gssMechTypes; /* DER of the client's MechTypeList (for the MIC) */ /* NTLMSSP (GSS-SPNEGO with NTLM) bind state */ bool ntlmInProgress = false; std::vector<uint8_t> ntlmChallenge; /* 8-byte server challenge */ std::shared_ptr<fenrirds::crypto::ntlm::Security> ntlmSec; /* sign+seal layer */ /* established SASL security layer (after a successful GSSAPI bind): * ssf 0 = none, 1 = integrity (sign), 2 = confidentiality (seal) */ int ssf = 0; Loading
plugin/krb5/krb5.cpp +34 −0 Changes for plugin/krb5/krb5.cpp: 34 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -3643,6 +3643,40 @@ std::vector<uint8_t> &Krb5KDC::realmTgsKey(RealmInfo &ri) { return ri.tgsKey; } bool Krb5KDC::ntlmNtHash(const std::string &user, const std::string &domain, std::vector<uint8_t> &ntHash, std::string &principal) { std::lock_guard<std::mutex> guard(_Lock); RealmInfo *ri = nullptr; if (!domain.empty()) { std::string du = dsdb::upper(domain); for (auto &kv : _Realms) { if (!kv.second.dir) continue; if (dsdb::upper(kv.second.meta.netbios) == du || dsdb::upper(kv.second.meta.dnsName) == du || dsdb::upper(kv.second.meta.realm) == du) { ri = &kv.second; break; } } } if (!ri) for (auto &kv : _Realms) if (kv.second.dir) { ri = &kv.second; break; } if (!ri || !ri->dir) return false; std::optional<dsdb::Entry> e = ri->dir->bySamAccountName(user); if (!e && user.find('@') != std::string::npos) e = ri->dir->byUpn(user); if (!e && !user.empty() && user.back() != '$') e = ri->dir->bySamAccountName(user + "$"); if (!e) return false; dsdb::Secrets sec = ri->dir->secretsOf(e->objectGuid); if (sec.ntHash.size() != 16) return false; ntHash.assign(sec.ntHash.begin(), sec.ntHash.end()); principal = user + "@" + ri->meta.realm; return true; } bool Krb5KDC::gssAccept(const std::vector<uint8_t> &apReqData, GssAcceptResult &out) { std::lock_guard<std::mutex> guard(_Lock); Loading
plugin/krb5/krb5.h +2 −0 Changes for plugin/krb5/krb5.h: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -732,6 +732,8 @@ namespace fenrirds { int etype) override; bool gssAccept(const std::vector<uint8_t> &apReq, GssAcceptResult &out) override; bool ntlmNtHash(const std::string &user, const std::string &domain, std::vector<uint8_t> &ntHash, std::string &principal) override; /* (Re-)load realms from the admin backend (called in ctor) */ void loadRealms(); Loading
plugin/smb/smb.cpp +82 −3 Changes for plugin/smb/smb.cpp: 82 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -7,6 +7,8 @@ #include "crypto/crypto.h" #include "crypto/gss.h" #include "crypto/ntlm.h" #include "dsdb/dn.h" #include "rpc/ndr.h" #include "rpc/services.h" #include "smb.h" Loading @@ -22,6 +24,9 @@ namespace { void w64(std::vector<uint8_t>&o,uint64_t v){for(int i=0;i<8;i++)o.push_back((v>>(8*i))&0xff);} constexpr uint32_t FSCTL_PIPE_TRANSCEIVE = 0x0011C017; /* NTLMSSP mech OID 1.3.6.1.4.1.311.2.2.10 (DER-encoded OID value). */ const uint8_t NTLMSSP_OID[] = {0x06,0x0a,0x2b,0x06,0x01,0x04,0x01,0x82,0x37,0x02,0x02,0x0a}; } /***************************************************************************** Loading Loading @@ -88,9 +93,16 @@ std::vector<uint8_t> RpcPipe::dispatch(const rpc::RequestPdu &req) { res.fault = true; res.faultCode = 0x1C010003; /* nca_unk_if */ } { static const char *hc = "0123456789abcdef"; std::string rq, rs; for (size_t i=0;i<req.stub.size() && i<64;i++){ rq+=hc[req.stub[i]>>4]; rq+=hc[req.stub[i]&0xf]; } for (size_t i=0;i<res.stub.size() && i<96;i++){ rs+=hc[res.stub[i]>>4]; rs+=hc[res.stub[i]&0xf]; } std::cerr << "[RPC] " << iface << " opnum=" << req.opnum << " stub=" << req.stub.size() << "B -> " << (res.fault ? "FAULT 0x" : "OK status=") << std::hex << (res.fault ? res.faultCode : res.status) << std::dec << std::endl; << std::hex << (res.fault ? res.faultCode : res.status) << std::dec << "\n req: " << rq << "\n res: " << rs << std::endl; } if (res.fault) return rpc::buildFault(req.callId, req.contextId, res.faultCode); return rpc::buildResponse(req.callId, req.contextId, res.stub); Loading Loading @@ -304,15 +316,82 @@ std::vector<uint8_t> SmbServer::handleMessage(Conn &c, const std::vector<uint8_t std::vector<uint8_t> apReq, mechTypes; bool spnego = false; std::vector<uint8_t> mt; std::vector<uint8_t> mt, mic; int nstate = 0; if (gss::parseSpnegoInit(tok, mt, mechTypes)) { spnego = true; apReq.swap(mt); } else if (gss::parseSpnegoResp(tok, mt, mic, nstate)) { spnego = true; apReq.swap(mt); } else { uint16_t id=0; gss::parseInitialToken(tok, id, apReq); } if (!g_KdcPlugin) return errorResponse(req, smb::STATUS_NOT_SUPPORTED); /* the AP-REQ inside (strip the krb5 GSS framing if present) */ /* strip any krb5 GSS framing (Kerberos AP-REQ is wrapped, NTLM is not) */ std::vector<uint8_t> inner = apReq; { uint16_t id=0; std::vector<uint8_t> x; if (gss::parseInitialToken(apReq, id, x)) inner.swap(x); } /* ---- NTLMSSP path (not-yet-domain-joined client) ---- */ namespace ntlm = crypto::ntlm; const std::vector<uint8_t> &ntok = ntlm::isNtlmssp(apReq) ? apReq : inner; if (ntlm::isNtlmssp(ntok)) { if (!c.sessionId) { auto r = crypto::randomBytes(8); c.sessionId = (rd64(r.data()) | 1); } uint32_t ty = ntlm::messageType(ntok); if (ty == 1) { c.ntlmChallenge = crypto::randomBytes(8); std::string dnsHost = _DcHost.empty() ? std::string("dc") : _DcHost; std::string dnsDomain = dnsHost.find('.') != std::string::npos ? dnsHost.substr(dnsHost.find('.')+1) : std::string(); std::string nbHost = dsdb::upper(dnsHost.substr(0, dnsHost.find('.'))); std::string nbDomain = dsdb::upper(dnsDomain.substr(0, dnsDomain.find('.'))); auto type2 = ntlm::buildChallenge(c.ntlmChallenge, nbDomain, nbHost, dnsDomain, dnsHost); std::vector<uint8_t> oid(NTLMSSP_OID, NTLMSSP_OID + sizeof(NTLMSSP_OID)); std::vector<uint8_t> outTok = spnego ? gss::buildSpnegoResp(gss::ACCEPT_INCOMPLETE, &oid, type2, {}) : type2; std::cerr << "[SMB] NTLMSSP NEGOTIATE -> CHALLENGE" << std::endl; auto resp = respHeader(smb::STATUS_MORE_PROCESSING); std::vector<uint8_t> b2; w16(b2, 9); w16(b2, 0); w16(b2, 72); w16(b2, uint16_t(outTok.size())); b2.insert(b2.end(), outTok.begin(), outTok.end()); resp.insert(resp.end(), b2.begin(), b2.end()); if (c.dialect >= smb::SMB_3_1_1) c.preauth = smb::preauthUpdate(c.preauth, resp.data(), resp.size()); return resp; /* unsigned: no session keys yet */ } if (ty == 3) { ntlm::Auth3 a; if (!ntlm::parseAuth3(ntok, a)) return errorResponse(req, smb::STATUS_LOGON_FAILURE); std::vector<uint8_t> ntHash; std::string princ; if (!g_KdcPlugin->ntlmNtHash(a.user, a.domain, ntHash, princ)) { std::cerr << "[SMB] NTLMSSP AUTH '" << a.domain << "\\" << a.user << "' -> UNKNOWN USER" << std::endl; return errorResponse(req, smb::STATUS_LOGON_FAILURE); } std::vector<uint8_t> exported; if (!ntlm::verifyV2(ntHash, a, c.ntlmChallenge, exported)) { std::cerr << "[SMB] NTLMSSP AUTH '" << a.domain << "\\" << a.user << "' -> BAD PASSWORD" << std::endl; return errorResponse(req, smb::STATUS_LOGON_FAILURE); } std::cerr << "[SMB] NTLMSSP AUTH '" << a.domain << "\\" << a.user << "' -> SUCCESS (" << princ << ")" << std::endl; c.authenticated = true; c.clientPrincipal = princ; c.sessionKey = exported; std::vector<uint8_t> outTok = spnego ? gss::buildSpnegoResp(gss::ACCEPT_COMPLETED, nullptr, {}, {}) : std::vector<uint8_t>(); auto resp = respHeader(smb::STATUS_SUCCESS); std::vector<uint8_t> b2; w16(b2, 9); w16(b2, 0); w16(b2, 72); w16(b2, uint16_t(outTok.size())); b2.insert(b2.end(), outTok.begin(), outTok.end()); resp.insert(resp.end(), b2.begin(), b2.end()); c.keys = smb::deriveKeys(c.dialect, exported, c.dialect >= smb::SMB_3_1_1 ? c.preauth : std::vector<uint8_t>()); c.haveKeys = true; return finish(resp); } return errorResponse(req, smb::STATUS_LOGON_FAILURE); } /* ---- Kerberos path ---- */ GssAcceptResult res; if (!g_KdcPlugin->gssAccept(inner, res)) { std::cerr << "[SMB] SESSION_SETUP gssAccept FAILED (apReq=" << inner.size() Loading