Loading plugin/krb5/krb5.cpp +281 −8 Changes for plugin/krb5/krb5.cpp: 281 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -26,6 +26,9 @@ *******************************************************************************/ #include <atomic> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/socket.h> #include <iostream> #include <cstring> #include <cstdlib> Loading Loading @@ -2646,6 +2649,11 @@ Krb5KDC::Krb5KDC(AuthBackend &adminBackend, /* UDP socket on the same port (Kerberos primary transport) */ _Udp = new netplus::udp(bindAddr.c_str(), port, maxconn, -1); std::cerr << "KDC listening on " << bindAddr << ":" << port << " (tcp+udp)" << std::endl; /* kpasswd (464): same framing over TCP, own UDP socket */ _ServerSockets.push_back(new netplus::tcp(bindAddr.c_str(), 464, maxconn, 0)); _UdpKpasswd = new netplus::udp(bindAddr.c_str(), 464, maxconn, -1); std::cerr << "kpasswd listening on " << bindAddr << ":464 (tcp+udp)" << std::endl; } void Krb5KDC::init() { Loading @@ -2656,23 +2664,28 @@ void Krb5KDC::init() { } if (_Udp) _Udp->bind(); if (_UdpKpasswd) _UdpKpasswd->bind(); } /* UDP responder: each datagram is a bare ASN.1 DER Kerberos message (no TCP * 4-byte length prefix). Process it and send the bare response back. */ void Krb5KDC::udpLoop() { void Krb5KDC::udpLoop(netplus::udp *sock) { while (_Running) { if (!_Udp->waitReadable(500)) if (!sock->waitReadable(500)) continue; std::vector<std::vector<uint8_t>> dgrams; std::vector<sockaddr_storage> addrs; size_t n = 0; try { n = _Udp->recvBatchAddr(dgrams, addrs, 16); n = sock->recvBatchAddr(dgrams, addrs, 16); } catch (...) { continue; } for (size_t i = 0; i < n; ++i) { std::vector<uint8_t> response; try { if (isKpasswd(dgrams[i].data(), dgrams[i].size())) response = handleKpasswd(dgrams[i].data(), dgrams[i].size(), _LocalIp); else response = handleStandardRequest(dgrams[i].data(), dgrams[i].size()); } catch (const std::exception &e) { std::string errRealm = _Realms.empty() ? "UNKNOWN" : _Realms.begin()->first; Loading @@ -2682,7 +2695,7 @@ void Krb5KDC::udpLoop() { continue; socklen_t sl = addrs[i].ss_family == AF_INET6 ? sizeof(sockaddr_in6) : sizeof(sockaddr_in); std::vector<std::pair<const uint8_t*, size_t>> out = {{response.data(), response.size()}}; try { _Udp->sendBatch(out, &addrs[i], sl); } catch (...) {} try { sock->sendBatch(out, &addrs[i], sl); } catch (...) {} } } } Loading @@ -2691,7 +2704,10 @@ Krb5KDC::~Krb5KDC() { _Running = false; if (_UdpThread.joinable()) _UdpThread.join(); if (_KpwThread.joinable()) _KpwThread.join(); delete _Udp; delete _UdpKpasswd; delete static_cast<KdcEvent*>(_Event); for (auto *s : _ServerSockets) delete s; Loading Loading @@ -2856,7 +2872,9 @@ bool Krb5KDC::getCrossRealmKey(const std::string &localRealm, void Krb5KDC::runKdc(ULONG_PTR args) { _Running = true; if (_Udp) _UdpThread = std::thread([this]() { udpLoop(); }); _UdpThread = std::thread([this]() { udpLoop(_Udp); }); if (_UdpKpasswd) _KpwThread = std::thread([this]() { udpLoop(_UdpKpasswd); }); auto *ev = new KdcEvent(_ServerSockets, this); _Event = ev; ev->runEventloop(args); Loading Loading @@ -2913,7 +2931,18 @@ void Krb5KDC::RequestEvent(netplus::con &curcon, const int /*tid*/, ULONG_PTR /* std::vector<uint8_t> response; try { if (isKpasswd(asn1Data, asn1Len)) { /* our address for the KRB-PRIV s-address */ sockaddr_in la{}; socklen_t ll = sizeof(la); uint32_t ip = _LocalIp; if (!curcon.slots.empty() && curcon.slots[0].csock && getsockname(curcon.slots[0].csock->fd(), reinterpret_cast<sockaddr*>(&la), &ll) == 0 && la.sin_family == AF_INET) ip = ntohl(la.sin_addr.s_addr); response = handleKpasswd(asn1Data, asn1Len, ip); } else { response = handleStandardRequest(asn1Data, asn1Len); } } catch (const std::exception &e) { std::string errRealm = _Realms.empty() ? "UNKNOWN" : _Realms.begin()->first; response = buildStandardError(KRB_ERR_GENERIC, e.what(), errRealm, "krbtgt"); Loading @@ -2934,6 +2963,229 @@ void Krb5KDC::RequestEvent(netplus::con &curcon, const int /*tid*/, ULONG_PTR /* curcon.RecvData.begin() + 4 + msg_len); } /***************************************************************************** * kpasswd (RFC 3244 / MS-KILE 3.1.5.12): change or set a password * * request: len(2) | version(2: 0x0001 change, 0xff80 set) | ap-req-len(2) * | AP-REQ (ticket for kadmin/changepw) | KRB-PRIV * reply: len(2) | version 1 | ap-rep-len(2) | AP-REP | KRB-PRIV(result) *****************************************************************************/ namespace { uint16_t be16(const uint8_t *p) { return uint16_t(p[0] << 8 | p[1]); } /* the DER content of context tag [n] inside a SEQUENCE content */ bool ctxField(const uint8_t *seq, size_t len, uint8_t n, const uint8_t *&c, size_t &cl) { size_t off = 0; uint8_t tag; const uint8_t *v; size_t vl; while (off < len && Asn1Der::parseTLV(seq, len, off, tag, v, vl)) { if (tag == uint8_t(0xA0 | n)) { c = v; cl = vl; return true; } } return false; } /* unwrap one TLV with the expected tag */ bool unTag(const uint8_t *d, size_t len, uint8_t want, const uint8_t *&c, size_t &cl) { size_t off = 0; uint8_t tag; return Asn1Der::parseTLV(d, len, off, tag, c, cl) && tag == want; } enum : uint16_t { KPW_SUCCESS = 0, KPW_MALFORMED = 1, KPW_HARDERROR = 2, KPW_AUTHERROR = 3, KPW_SOFTERROR = 4, KPW_ACCESSDENIED = 5, KPW_BAD_VERSION = 6, KPW_INITIAL_FLAG_NEEDED = 7, }; std::vector<uint8_t> frameKpasswd(const std::vector<uint8_t> &apRep, const std::vector<uint8_t> &body) { std::vector<uint8_t> o; size_t total = 6 + apRep.size() + body.size(); o.push_back(uint8_t(total >> 8)); o.push_back(uint8_t(total)); o.push_back(0); o.push_back(1); /* version 1 */ o.push_back(uint8_t(apRep.size() >> 8)); o.push_back(uint8_t(apRep.size())); o.insert(o.end(), apRep.begin(), apRep.end()); o.insert(o.end(), body.begin(), body.end()); return o; } } void Krb5KDC::setLocalAddress(const std::string &ipv4) { in_addr a{}; if (inet_pton(AF_INET, ipv4.c_str(), &a) == 1) _LocalIp = ntohl(a.s_addr); } bool Krb5KDC::isKpasswd(const uint8_t *data, size_t len) { if (len < 6 || data[0] == Asn1Der::APP_AS_REQ || data[0] == Asn1Der::APP_TGS_REQ) return false; uint16_t ver = be16(data + 2); return be16(data) == len && (ver == 0x0001 || ver == 0xff80); } std::vector<uint8_t> Krb5KDC::handleKpasswd(const uint8_t *data, size_t len, uint32_t localIp) { std::lock_guard<std::mutex> guard(_Lock); std::string anyRealm = _Realms.empty() ? "UNKNOWN" : _Realms.begin()->first; auto fail = [&](uint16_t code, const std::string &text) { std::cerr << "[KPASSWD] error " << code << ": " << text << std::endl; Krb5Error err; err.pvno = 5; err.msg_type = KRB_ERROR; err.stime = time(nullptr); err.susec = 0; err.error_code = KRB_ERR_GENERIC; err.sname = Krb5Principal(KRB5_NT_SRV_INST, anyRealm, "kadmin", "changepw"); err.e_text = text; err.e_data = {uint8_t(code >> 8), uint8_t(code)}; err.e_data.insert(err.e_data.end(), text.begin(), text.end()); return frameKpasswd({}, err.serialize()); }; uint16_t version = be16(data + 2), apLen = be16(data + 4); if (6u + apLen > len) return fail(KPW_MALFORMED, "truncated request"); Krb5ApReq ap; size_t used = 0; if (!ap.deserialize(data + 6, apLen, used)) return fail(KPW_MALFORMED, "bad AP-REQ"); RealmInfo *ri = resolveRealm(ap.ticket.sname.realm); if (!ri || !ri->dir) return fail(KPW_HARDERROR, "unknown realm"); bool changepwTkt = ap.ticket.sname.name == "kadmin" && ap.ticket.sname.instance == "changepw"; bool tgtTkt = ap.ticket.sname.name == "krbtgt"; if (!changepwTkt && !tgtTkt) return fail(KPW_AUTHERROR, "ticket is not for kadmin/changepw"); /* kadmin/changepw shares the krbtgt key, as in AD */ Krb5EncTicketPart et; try { auto p = Krb5Crypto::decrypt(realmTgsKey(*ri), ap.ticket.enc_part, KRB5_KU_TICKET); size_t cc = 0; if (!et.deserialize(p.data(), p.size(), cc)) return fail(KPW_AUTHERROR, "bad ticket"); } catch (const std::exception &) { return fail(KPW_AUTHERROR, "ticket does not decrypt"); } time_t now = time(nullptr); if (now > et.endtime) return fail(KPW_AUTHERROR, "ticket expired"); Krb5Authenticator auth; try { auto a = Krb5Crypto::decrypt(et.session_key.keydata, ap.authenticator, KRB5_KU_AP_REQ_AUTH); size_t cc = 0; if (!auth.deserialize(a.data(), a.size(), cc)) return fail(KPW_AUTHERROR, "bad authenticator"); } catch (const std::exception &) { return fail(KPW_AUTHERROR, "authenticator does not decrypt"); } if (std::abs(long(now - auth.ctime)) > _MaxClockSkew) return fail(KPW_AUTHERROR, "clock skew"); /* KRB-PRIV in both directions: the authenticator subkey, else the session key */ Krb5SessionKey privKey = (auth.has_subkey && !auth.subkey.keydata.empty()) ? auth.subkey : et.session_key; /* KRB-PRIV ::= [APPLICATION 21] SEQUENCE { pvno[0], msg-type[1], enc-part[3] } */ const uint8_t *seq, *f; size_t seqLen, fl; const uint8_t *pd = data + 6 + apLen; size_t pdl = len - 6 - apLen; if (!unTag(pd, pdl, 0x75, seq, seqLen) || !unTag(seq, seqLen, 0x30, seq, seqLen) || !ctxField(seq, seqLen, 3, f, fl)) return fail(KPW_MALFORMED, "bad KRB-PRIV"); Krb5EncType et2; uint32_t kv; std::vector<uint8_t> cipher; if (!Asn1Der::parseEncryptedData(f, fl, et2, kv, cipher)) return fail(KPW_MALFORMED, "bad KRB-PRIV enc-part"); std::vector<uint8_t> plain; try { plain = Krb5Crypto::decrypt(privKey.keydata, cipher, KRB5_KU_KRB_PRIV_ENC_PART); } catch (const std::exception &) { return fail(KPW_AUTHERROR, "KRB-PRIV does not decrypt"); } /* EncKrbPrivPart ::= [APPLICATION 28] SEQUENCE { user-data[0] OCTET STRING, ... } */ if (!unTag(plain.data(), plain.size(), 0x7C, seq, seqLen) || !unTag(seq, seqLen, 0x30, seq, seqLen) || !ctxField(seq, seqLen, 0, f, fl)) return fail(KPW_MALFORMED, "bad EncKrbPrivPart"); std::vector<uint8_t> userData; if (!unTag(f, fl, 0x04, f, fl)) return fail(KPW_MALFORMED, "bad user-data"); userData.assign(f, f + fl); /* the new password, and for set-password an optional target */ std::string newPw; std::string targetName; if (version == 0x0001) { newPw.assign(userData.begin(), userData.end()); } else { /* ChangePasswdData ::= SEQUENCE { newpasswd[0] OCTET STRING, * targname[1] PrincipalName OPTIONAL, targrealm[2] Realm OPTIONAL } */ const uint8_t *cs; size_t csl; if (!unTag(userData.data(), userData.size(), 0x30, cs, csl) || !ctxField(cs, csl, 0, f, fl) || !unTag(f, fl, 0x04, f, fl)) return fail(KPW_MALFORMED, "bad ChangePasswdData"); newPw.assign(f, f + fl); if (ctxField(cs, csl, 1, f, fl)) { Krb5PrincipalType pt; std::vector<std::string> comps; if (Asn1Der::parsePrincipalName(f, fl, pt, comps) && !comps.empty()) { for (auto &c : comps) { if (!targetName.empty()) targetName += "/"; targetName += c; } } } } /* who is asking, and for whom */ auto findAcct = [&](const std::string &name) -> std::optional<dsdb::Entry> { auto e = ri->dir->bySamAccountName(name); if (!e) e = ri->dir->byUpn(name); if (!e && name.find('@') != std::string::npos) e = ri->dir->bySamAccountName(name.substr(0, name.find('@'))); if (!e && !name.empty() && name.back() != '$') e = ri->dir->bySamAccountName(name + "$"); return e; }; auto caller = findAcct(et.cname.name); if (!caller) return fail(KPW_AUTHERROR, "unknown client"); std::optional<dsdb::Entry> target = caller; if (!targetName.empty()) { target = findAcct(targetName); if (!target) return fail(KPW_HARDERROR, "unknown target principal"); } bool self = target->objectGuid == caller->objectGuid; uint16_t code = KPW_SUCCESS; std::string text = "Password changed"; if (self && !(et.flags & KRB5_TF_INITIAL)) { code = KPW_INITIAL_FLAG_NEEDED; text = "Initial ticket required"; } else if (!self) { /* setting another account's password: Domain Admins / Administrator */ bool admin = !caller->objectSid.empty() && caller->objectSid.rid() == 500; dsdb::Dn da = ri->meta.baseDn.child("CN", "Users").child("CN", "Domain Admins"); if (auto *mo = caller->get("memberOf")) for (auto &g : *mo) { dsdb::Dn gd; if (dsdb::Dn::parse(g, gd) && gd == da) admin = true; } if (!admin) { code = KPW_ACCESSDENIED; text = "Not allowed to set this password"; } } if (code == KPW_SUCCESS) { /* the domain's minimum length (minPwdLength on the domain head, AD default 7) */ int64_t minLen = 7; if (auto head = ri->dir->byDn(ri->meta.baseDn)) minLen = dsdb::intValue(head->attrs, "minPwdLength", 7); size_t chars = crypto::utf16le(newPw).size() / 2; if (newPw.empty() || int64_t(chars) < minLen) { code = KPW_SOFTERROR; text = "Password must be at least " + std::to_string(minLen) + " characters"; } } if (code == KPW_SUCCESS) { std::string sam = target->str("sAMAccountName"); bool computer = target->isClass("computer"); std::string salt = dsdb::kerberosSalt(computer ? dsdb::PrincipalKind::Computer : dsdb::PrincipalKind::User, ri->meta.realm, sam, ri->meta.dnsName); dsdb::Secrets old = ri->dir->secretsOf(target->objectGuid); dsdb::Secrets sec = dsdb::deriveSecrets(newPw, salt, old.kvno); dsdb::AttrMap a; dsdb::setInt(a, "pwdLastSet", sec.pwdLastSet); dsdb::setInt(a, "msDS-KeyVersionNumber", sec.kvno); try { ri->dir->modify(target->objectGuid, a, &sec, true); } catch (const std::exception &e) { code = KPW_HARDERROR; text = e.what(); } } std::cerr << "[KPASSWD] " << et.cname.name << " -> " << target->str("sAMAccountName") << " version=0x" << std::hex << version << std::dec << " result=" << code << " (" << text << ")" << std::endl; /* AP-REP (no subkey: the replies stay under the client's key) */ uint32_t seqNo = Krb5Crypto::randomNonce() & 0x3fffffff; Krb5Service svc("", "", ri->meta.realm, realmTgsKey(*ri)); std::vector<uint8_t> apRep = svc.buildApRep(auth, et.session_key, seqNo, nullptr); /* KRB-PRIV with result code + text */ std::vector<uint8_t> result = {uint8_t(code >> 8), uint8_t(code)}; result.insert(result.end(), text.begin(), text.end()); std::vector<uint8_t> addr; Asn1Der::encodeContextTag(addr, 0, Asn1Der::encodeInteger(2)); /* IPv4 */ Asn1Der::encodeContextTag(addr, 1, Asn1Der::encodeOctetString( {uint8_t(localIp >> 24), uint8_t(localIp >> 16), uint8_t(localIp >> 8), uint8_t(localIp)})); std::vector<uint8_t> encPart; Asn1Der::encodeContextTag(encPart, 0, Asn1Der::encodeOctetString(result)); Asn1Der::encodeContextTag(encPart, 1, Asn1Der::encodeGeneralizedTime(now)); Asn1Der::encodeContextTag(encPart, 2, Asn1Der::encodeUInteger(0)); Asn1Der::encodeContextTag(encPart, 3, Asn1Der::encodeUInteger(seqNo)); Asn1Der::encodeContextTag(encPart, 4, Asn1Der::encodeSequence(addr)); std::vector<uint8_t> encPrivPart; Asn1Der::encodeTLV(encPrivPart, 0x7C, Asn1Der::encodeSequence(encPart)); auto privCipher = Krb5Crypto::encrypt(privKey.keydata, encPrivPart, KRB5_KU_KRB_PRIV_ENC_PART); std::vector<uint8_t> priv; Asn1Der::encodeContextTag(priv, 0, Asn1Der::encodeInteger(5)); Asn1Der::encodeContextTag(priv, 1, Asn1Der::encodeInteger(21)); Asn1Der::encodeContextTag(priv, 3, Asn1Der::encodeEncryptedData(privKey.enctype, 0, privCipher)); std::vector<uint8_t> krbPriv; Asn1Der::encodeTLV(krbPriv, 0x75, Asn1Der::encodeSequence(priv)); return frameKpasswd(apRep, krbPriv); } /* * handleStandardRequest - Dispatch ASN.1 DER encoded Kerberos messages */ Loading Loading @@ -3171,6 +3423,22 @@ std::vector<uint8_t> Krb5KDC::handleStandardAsReq(const uint8_t *data, size_t le } } /* a ticket for the password-change service (kadmin/changepw, the * krbtgt key in AD) is what an expired password still gets */ bool changepw = req.sname.name == "kadmin" && req.sname.instance == "changepw"; /* pwdLastSet = 0: "must change password at next logon" */ if (!changepw) { if (auto who = ri->dir ? ri->dir->byGuid(uid) : std::nullopt) { auto *pls = who->get("pwdLastSet"); if (pls && !pls->empty() && pls->front() == "0") { std::cerr << "[KDC] AS-REQ: password of '" << who->str("sAMAccountName") << "' must be changed (pwdLastSet=0)" << std::endl; return buildStandardError(KDC_ERR_KEY_EXPIRED, "Password has expired", req.cname.realm, "krbtgt"); } } } /* Generate TGT session key */ Krb5SessionKey tgt_session_key = Krb5Crypto::generateSessionKey(selectedEtype); Loading Loading @@ -3251,7 +3519,8 @@ std::vector<uint8_t> Krb5KDC::handleStandardAsReq(const uint8_t *data, size_t le /* Build TGT */ Krb5Ticket tgt; tgt.tkt_vno = 5; tgt.sname = Krb5Principal(KRB5_NT_SRV_INST, req.cname.realm, "krbtgt", req.cname.realm); tgt.sname = changepw ? Krb5Principal(KRB5_NT_SRV_INST, req.cname.realm, "kadmin", "changepw") : Krb5Principal(KRB5_NT_SRV_INST, req.cname.realm, "krbtgt", req.cname.realm); tgt.enc_etype = selectedEtype; tgt.enc_kvno = 1; tgt.enc_part = enc_tkt_cipher; Loading Loading @@ -4542,7 +4811,7 @@ extern "C" { fenrirds::Plugin *fenrirds_plugin_create(fenrirds::AuthBackend &backend, const char *configJson) { std::string bindAddr = "0.0.0.0"; std::string bindAddr = "0.0.0.0", address; int port = 88; int maxconn = 128; Loading @@ -4555,11 +4824,15 @@ fenrirds::Plugin *fenrirds_plugin_create(fenrirds::AuthBackend &backend, if (jbind) bindAddr = json_object_get_string(jbind); if (jport) port = json_object_get_int(jport); if (jmax) maxconn = json_object_get_int(jmax); if (auto *ja = json_object_object_get(jobj, "dns_address")) address = json_object_get_string(ja); json_object_put(jobj); } } return new fenrirds::Krb5KDC(backend, bindAddr, port, maxconn); auto *kdc = new fenrirds::Krb5KDC(backend, bindAddr, port, maxconn); if (!address.empty()) kdc->setLocalAddress(address); return kdc; } void fenrirds_plugin_destroy(fenrirds::Plugin *p) { Loading plugin/krb5/krb5.h +14 −1 Changes for plugin/krb5/krb5.h: 14 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -89,6 +89,7 @@ namespace fenrirds { KDC_ERR_NEVER_VALID = 11, KDC_ERR_POLICY = 12, KDC_ERR_BADOPTION = 13, KDC_ERR_KEY_EXPIRED = 23, KDC_ERR_PREAUTH_FAILED = 24, KDC_ERR_PREAUTH_REQUIRED = 25, KDC_ERR_WRONG_REALM = 68, Loading Loading @@ -144,6 +145,7 @@ namespace fenrirds { KRB5_KU_TGS_REP_ENC_PART = 8, /* TGS-REP encrypted part */ KRB5_KU_TGS_REP_ENC_PART_SUB = 9, /* TGS-REP enc part (subkey) */ KRB5_KU_AP_REP_ENC_PART = 12, /* AP-REP encrypted part */ KRB5_KU_KRB_PRIV_ENC_PART = 13, /* KRB-PRIV encrypted part (kpasswd) */ KRB5_KU_TICKET = 2 /* Ticket encrypted part */ }; Loading Loading @@ -770,6 +772,14 @@ namespace fenrirds { private: /* Resolve the realm from a request and return the RealmInfo, or nullptr */ RealmInfo *resolveRealm(const std::string &realm); /* kpasswd (RFC 3244, port 464): change/set password; localIp * (host order) is our address for the KRB-PRIV s-address */ std::vector<uint8_t> handleKpasswd(const uint8_t *data, size_t len, uint32_t localIp); static bool isKpasswd(const uint8_t *data, size_t len); public: /* the DC's IPv4 address (DNS/ADDRESS) for UDP kpasswd replies */ void setLocalAddress(const std::string &ipv4); private: void canonicalizeRealms(Krb5KdcReq &req); /* Standard Kerberos protocol (ASN.1 DER / RFC 4120) handlers */ Loading Loading @@ -829,8 +839,11 @@ namespace fenrirds { * Windows sends AS-REQ over UDP first. Served in its own thread. */ netplus::udp *_Udp = nullptr; std::thread _UdpThread; std::thread _KpwThread; netplus::udp *_UdpKpasswd = nullptr; uint32_t _LocalIp = 0; /* DC address (host order), UDP kpasswd replies */ std::atomic<bool> _Running{false}; void udpLoop(); void udpLoop(netplus::udp *sock); }; /*************************************************************************** Loading Loading
plugin/krb5/krb5.cpp +281 −8 Changes for plugin/krb5/krb5.cpp: 281 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -26,6 +26,9 @@ *******************************************************************************/ #include <atomic> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/socket.h> #include <iostream> #include <cstring> #include <cstdlib> Loading Loading @@ -2646,6 +2649,11 @@ Krb5KDC::Krb5KDC(AuthBackend &adminBackend, /* UDP socket on the same port (Kerberos primary transport) */ _Udp = new netplus::udp(bindAddr.c_str(), port, maxconn, -1); std::cerr << "KDC listening on " << bindAddr << ":" << port << " (tcp+udp)" << std::endl; /* kpasswd (464): same framing over TCP, own UDP socket */ _ServerSockets.push_back(new netplus::tcp(bindAddr.c_str(), 464, maxconn, 0)); _UdpKpasswd = new netplus::udp(bindAddr.c_str(), 464, maxconn, -1); std::cerr << "kpasswd listening on " << bindAddr << ":464 (tcp+udp)" << std::endl; } void Krb5KDC::init() { Loading @@ -2656,23 +2664,28 @@ void Krb5KDC::init() { } if (_Udp) _Udp->bind(); if (_UdpKpasswd) _UdpKpasswd->bind(); } /* UDP responder: each datagram is a bare ASN.1 DER Kerberos message (no TCP * 4-byte length prefix). Process it and send the bare response back. */ void Krb5KDC::udpLoop() { void Krb5KDC::udpLoop(netplus::udp *sock) { while (_Running) { if (!_Udp->waitReadable(500)) if (!sock->waitReadable(500)) continue; std::vector<std::vector<uint8_t>> dgrams; std::vector<sockaddr_storage> addrs; size_t n = 0; try { n = _Udp->recvBatchAddr(dgrams, addrs, 16); n = sock->recvBatchAddr(dgrams, addrs, 16); } catch (...) { continue; } for (size_t i = 0; i < n; ++i) { std::vector<uint8_t> response; try { if (isKpasswd(dgrams[i].data(), dgrams[i].size())) response = handleKpasswd(dgrams[i].data(), dgrams[i].size(), _LocalIp); else response = handleStandardRequest(dgrams[i].data(), dgrams[i].size()); } catch (const std::exception &e) { std::string errRealm = _Realms.empty() ? "UNKNOWN" : _Realms.begin()->first; Loading @@ -2682,7 +2695,7 @@ void Krb5KDC::udpLoop() { continue; socklen_t sl = addrs[i].ss_family == AF_INET6 ? sizeof(sockaddr_in6) : sizeof(sockaddr_in); std::vector<std::pair<const uint8_t*, size_t>> out = {{response.data(), response.size()}}; try { _Udp->sendBatch(out, &addrs[i], sl); } catch (...) {} try { sock->sendBatch(out, &addrs[i], sl); } catch (...) {} } } } Loading @@ -2691,7 +2704,10 @@ Krb5KDC::~Krb5KDC() { _Running = false; if (_UdpThread.joinable()) _UdpThread.join(); if (_KpwThread.joinable()) _KpwThread.join(); delete _Udp; delete _UdpKpasswd; delete static_cast<KdcEvent*>(_Event); for (auto *s : _ServerSockets) delete s; Loading Loading @@ -2856,7 +2872,9 @@ bool Krb5KDC::getCrossRealmKey(const std::string &localRealm, void Krb5KDC::runKdc(ULONG_PTR args) { _Running = true; if (_Udp) _UdpThread = std::thread([this]() { udpLoop(); }); _UdpThread = std::thread([this]() { udpLoop(_Udp); }); if (_UdpKpasswd) _KpwThread = std::thread([this]() { udpLoop(_UdpKpasswd); }); auto *ev = new KdcEvent(_ServerSockets, this); _Event = ev; ev->runEventloop(args); Loading Loading @@ -2913,7 +2931,18 @@ void Krb5KDC::RequestEvent(netplus::con &curcon, const int /*tid*/, ULONG_PTR /* std::vector<uint8_t> response; try { if (isKpasswd(asn1Data, asn1Len)) { /* our address for the KRB-PRIV s-address */ sockaddr_in la{}; socklen_t ll = sizeof(la); uint32_t ip = _LocalIp; if (!curcon.slots.empty() && curcon.slots[0].csock && getsockname(curcon.slots[0].csock->fd(), reinterpret_cast<sockaddr*>(&la), &ll) == 0 && la.sin_family == AF_INET) ip = ntohl(la.sin_addr.s_addr); response = handleKpasswd(asn1Data, asn1Len, ip); } else { response = handleStandardRequest(asn1Data, asn1Len); } } catch (const std::exception &e) { std::string errRealm = _Realms.empty() ? "UNKNOWN" : _Realms.begin()->first; response = buildStandardError(KRB_ERR_GENERIC, e.what(), errRealm, "krbtgt"); Loading @@ -2934,6 +2963,229 @@ void Krb5KDC::RequestEvent(netplus::con &curcon, const int /*tid*/, ULONG_PTR /* curcon.RecvData.begin() + 4 + msg_len); } /***************************************************************************** * kpasswd (RFC 3244 / MS-KILE 3.1.5.12): change or set a password * * request: len(2) | version(2: 0x0001 change, 0xff80 set) | ap-req-len(2) * | AP-REQ (ticket for kadmin/changepw) | KRB-PRIV * reply: len(2) | version 1 | ap-rep-len(2) | AP-REP | KRB-PRIV(result) *****************************************************************************/ namespace { uint16_t be16(const uint8_t *p) { return uint16_t(p[0] << 8 | p[1]); } /* the DER content of context tag [n] inside a SEQUENCE content */ bool ctxField(const uint8_t *seq, size_t len, uint8_t n, const uint8_t *&c, size_t &cl) { size_t off = 0; uint8_t tag; const uint8_t *v; size_t vl; while (off < len && Asn1Der::parseTLV(seq, len, off, tag, v, vl)) { if (tag == uint8_t(0xA0 | n)) { c = v; cl = vl; return true; } } return false; } /* unwrap one TLV with the expected tag */ bool unTag(const uint8_t *d, size_t len, uint8_t want, const uint8_t *&c, size_t &cl) { size_t off = 0; uint8_t tag; return Asn1Der::parseTLV(d, len, off, tag, c, cl) && tag == want; } enum : uint16_t { KPW_SUCCESS = 0, KPW_MALFORMED = 1, KPW_HARDERROR = 2, KPW_AUTHERROR = 3, KPW_SOFTERROR = 4, KPW_ACCESSDENIED = 5, KPW_BAD_VERSION = 6, KPW_INITIAL_FLAG_NEEDED = 7, }; std::vector<uint8_t> frameKpasswd(const std::vector<uint8_t> &apRep, const std::vector<uint8_t> &body) { std::vector<uint8_t> o; size_t total = 6 + apRep.size() + body.size(); o.push_back(uint8_t(total >> 8)); o.push_back(uint8_t(total)); o.push_back(0); o.push_back(1); /* version 1 */ o.push_back(uint8_t(apRep.size() >> 8)); o.push_back(uint8_t(apRep.size())); o.insert(o.end(), apRep.begin(), apRep.end()); o.insert(o.end(), body.begin(), body.end()); return o; } } void Krb5KDC::setLocalAddress(const std::string &ipv4) { in_addr a{}; if (inet_pton(AF_INET, ipv4.c_str(), &a) == 1) _LocalIp = ntohl(a.s_addr); } bool Krb5KDC::isKpasswd(const uint8_t *data, size_t len) { if (len < 6 || data[0] == Asn1Der::APP_AS_REQ || data[0] == Asn1Der::APP_TGS_REQ) return false; uint16_t ver = be16(data + 2); return be16(data) == len && (ver == 0x0001 || ver == 0xff80); } std::vector<uint8_t> Krb5KDC::handleKpasswd(const uint8_t *data, size_t len, uint32_t localIp) { std::lock_guard<std::mutex> guard(_Lock); std::string anyRealm = _Realms.empty() ? "UNKNOWN" : _Realms.begin()->first; auto fail = [&](uint16_t code, const std::string &text) { std::cerr << "[KPASSWD] error " << code << ": " << text << std::endl; Krb5Error err; err.pvno = 5; err.msg_type = KRB_ERROR; err.stime = time(nullptr); err.susec = 0; err.error_code = KRB_ERR_GENERIC; err.sname = Krb5Principal(KRB5_NT_SRV_INST, anyRealm, "kadmin", "changepw"); err.e_text = text; err.e_data = {uint8_t(code >> 8), uint8_t(code)}; err.e_data.insert(err.e_data.end(), text.begin(), text.end()); return frameKpasswd({}, err.serialize()); }; uint16_t version = be16(data + 2), apLen = be16(data + 4); if (6u + apLen > len) return fail(KPW_MALFORMED, "truncated request"); Krb5ApReq ap; size_t used = 0; if (!ap.deserialize(data + 6, apLen, used)) return fail(KPW_MALFORMED, "bad AP-REQ"); RealmInfo *ri = resolveRealm(ap.ticket.sname.realm); if (!ri || !ri->dir) return fail(KPW_HARDERROR, "unknown realm"); bool changepwTkt = ap.ticket.sname.name == "kadmin" && ap.ticket.sname.instance == "changepw"; bool tgtTkt = ap.ticket.sname.name == "krbtgt"; if (!changepwTkt && !tgtTkt) return fail(KPW_AUTHERROR, "ticket is not for kadmin/changepw"); /* kadmin/changepw shares the krbtgt key, as in AD */ Krb5EncTicketPart et; try { auto p = Krb5Crypto::decrypt(realmTgsKey(*ri), ap.ticket.enc_part, KRB5_KU_TICKET); size_t cc = 0; if (!et.deserialize(p.data(), p.size(), cc)) return fail(KPW_AUTHERROR, "bad ticket"); } catch (const std::exception &) { return fail(KPW_AUTHERROR, "ticket does not decrypt"); } time_t now = time(nullptr); if (now > et.endtime) return fail(KPW_AUTHERROR, "ticket expired"); Krb5Authenticator auth; try { auto a = Krb5Crypto::decrypt(et.session_key.keydata, ap.authenticator, KRB5_KU_AP_REQ_AUTH); size_t cc = 0; if (!auth.deserialize(a.data(), a.size(), cc)) return fail(KPW_AUTHERROR, "bad authenticator"); } catch (const std::exception &) { return fail(KPW_AUTHERROR, "authenticator does not decrypt"); } if (std::abs(long(now - auth.ctime)) > _MaxClockSkew) return fail(KPW_AUTHERROR, "clock skew"); /* KRB-PRIV in both directions: the authenticator subkey, else the session key */ Krb5SessionKey privKey = (auth.has_subkey && !auth.subkey.keydata.empty()) ? auth.subkey : et.session_key; /* KRB-PRIV ::= [APPLICATION 21] SEQUENCE { pvno[0], msg-type[1], enc-part[3] } */ const uint8_t *seq, *f; size_t seqLen, fl; const uint8_t *pd = data + 6 + apLen; size_t pdl = len - 6 - apLen; if (!unTag(pd, pdl, 0x75, seq, seqLen) || !unTag(seq, seqLen, 0x30, seq, seqLen) || !ctxField(seq, seqLen, 3, f, fl)) return fail(KPW_MALFORMED, "bad KRB-PRIV"); Krb5EncType et2; uint32_t kv; std::vector<uint8_t> cipher; if (!Asn1Der::parseEncryptedData(f, fl, et2, kv, cipher)) return fail(KPW_MALFORMED, "bad KRB-PRIV enc-part"); std::vector<uint8_t> plain; try { plain = Krb5Crypto::decrypt(privKey.keydata, cipher, KRB5_KU_KRB_PRIV_ENC_PART); } catch (const std::exception &) { return fail(KPW_AUTHERROR, "KRB-PRIV does not decrypt"); } /* EncKrbPrivPart ::= [APPLICATION 28] SEQUENCE { user-data[0] OCTET STRING, ... } */ if (!unTag(plain.data(), plain.size(), 0x7C, seq, seqLen) || !unTag(seq, seqLen, 0x30, seq, seqLen) || !ctxField(seq, seqLen, 0, f, fl)) return fail(KPW_MALFORMED, "bad EncKrbPrivPart"); std::vector<uint8_t> userData; if (!unTag(f, fl, 0x04, f, fl)) return fail(KPW_MALFORMED, "bad user-data"); userData.assign(f, f + fl); /* the new password, and for set-password an optional target */ std::string newPw; std::string targetName; if (version == 0x0001) { newPw.assign(userData.begin(), userData.end()); } else { /* ChangePasswdData ::= SEQUENCE { newpasswd[0] OCTET STRING, * targname[1] PrincipalName OPTIONAL, targrealm[2] Realm OPTIONAL } */ const uint8_t *cs; size_t csl; if (!unTag(userData.data(), userData.size(), 0x30, cs, csl) || !ctxField(cs, csl, 0, f, fl) || !unTag(f, fl, 0x04, f, fl)) return fail(KPW_MALFORMED, "bad ChangePasswdData"); newPw.assign(f, f + fl); if (ctxField(cs, csl, 1, f, fl)) { Krb5PrincipalType pt; std::vector<std::string> comps; if (Asn1Der::parsePrincipalName(f, fl, pt, comps) && !comps.empty()) { for (auto &c : comps) { if (!targetName.empty()) targetName += "/"; targetName += c; } } } } /* who is asking, and for whom */ auto findAcct = [&](const std::string &name) -> std::optional<dsdb::Entry> { auto e = ri->dir->bySamAccountName(name); if (!e) e = ri->dir->byUpn(name); if (!e && name.find('@') != std::string::npos) e = ri->dir->bySamAccountName(name.substr(0, name.find('@'))); if (!e && !name.empty() && name.back() != '$') e = ri->dir->bySamAccountName(name + "$"); return e; }; auto caller = findAcct(et.cname.name); if (!caller) return fail(KPW_AUTHERROR, "unknown client"); std::optional<dsdb::Entry> target = caller; if (!targetName.empty()) { target = findAcct(targetName); if (!target) return fail(KPW_HARDERROR, "unknown target principal"); } bool self = target->objectGuid == caller->objectGuid; uint16_t code = KPW_SUCCESS; std::string text = "Password changed"; if (self && !(et.flags & KRB5_TF_INITIAL)) { code = KPW_INITIAL_FLAG_NEEDED; text = "Initial ticket required"; } else if (!self) { /* setting another account's password: Domain Admins / Administrator */ bool admin = !caller->objectSid.empty() && caller->objectSid.rid() == 500; dsdb::Dn da = ri->meta.baseDn.child("CN", "Users").child("CN", "Domain Admins"); if (auto *mo = caller->get("memberOf")) for (auto &g : *mo) { dsdb::Dn gd; if (dsdb::Dn::parse(g, gd) && gd == da) admin = true; } if (!admin) { code = KPW_ACCESSDENIED; text = "Not allowed to set this password"; } } if (code == KPW_SUCCESS) { /* the domain's minimum length (minPwdLength on the domain head, AD default 7) */ int64_t minLen = 7; if (auto head = ri->dir->byDn(ri->meta.baseDn)) minLen = dsdb::intValue(head->attrs, "minPwdLength", 7); size_t chars = crypto::utf16le(newPw).size() / 2; if (newPw.empty() || int64_t(chars) < minLen) { code = KPW_SOFTERROR; text = "Password must be at least " + std::to_string(minLen) + " characters"; } } if (code == KPW_SUCCESS) { std::string sam = target->str("sAMAccountName"); bool computer = target->isClass("computer"); std::string salt = dsdb::kerberosSalt(computer ? dsdb::PrincipalKind::Computer : dsdb::PrincipalKind::User, ri->meta.realm, sam, ri->meta.dnsName); dsdb::Secrets old = ri->dir->secretsOf(target->objectGuid); dsdb::Secrets sec = dsdb::deriveSecrets(newPw, salt, old.kvno); dsdb::AttrMap a; dsdb::setInt(a, "pwdLastSet", sec.pwdLastSet); dsdb::setInt(a, "msDS-KeyVersionNumber", sec.kvno); try { ri->dir->modify(target->objectGuid, a, &sec, true); } catch (const std::exception &e) { code = KPW_HARDERROR; text = e.what(); } } std::cerr << "[KPASSWD] " << et.cname.name << " -> " << target->str("sAMAccountName") << " version=0x" << std::hex << version << std::dec << " result=" << code << " (" << text << ")" << std::endl; /* AP-REP (no subkey: the replies stay under the client's key) */ uint32_t seqNo = Krb5Crypto::randomNonce() & 0x3fffffff; Krb5Service svc("", "", ri->meta.realm, realmTgsKey(*ri)); std::vector<uint8_t> apRep = svc.buildApRep(auth, et.session_key, seqNo, nullptr); /* KRB-PRIV with result code + text */ std::vector<uint8_t> result = {uint8_t(code >> 8), uint8_t(code)}; result.insert(result.end(), text.begin(), text.end()); std::vector<uint8_t> addr; Asn1Der::encodeContextTag(addr, 0, Asn1Der::encodeInteger(2)); /* IPv4 */ Asn1Der::encodeContextTag(addr, 1, Asn1Der::encodeOctetString( {uint8_t(localIp >> 24), uint8_t(localIp >> 16), uint8_t(localIp >> 8), uint8_t(localIp)})); std::vector<uint8_t> encPart; Asn1Der::encodeContextTag(encPart, 0, Asn1Der::encodeOctetString(result)); Asn1Der::encodeContextTag(encPart, 1, Asn1Der::encodeGeneralizedTime(now)); Asn1Der::encodeContextTag(encPart, 2, Asn1Der::encodeUInteger(0)); Asn1Der::encodeContextTag(encPart, 3, Asn1Der::encodeUInteger(seqNo)); Asn1Der::encodeContextTag(encPart, 4, Asn1Der::encodeSequence(addr)); std::vector<uint8_t> encPrivPart; Asn1Der::encodeTLV(encPrivPart, 0x7C, Asn1Der::encodeSequence(encPart)); auto privCipher = Krb5Crypto::encrypt(privKey.keydata, encPrivPart, KRB5_KU_KRB_PRIV_ENC_PART); std::vector<uint8_t> priv; Asn1Der::encodeContextTag(priv, 0, Asn1Der::encodeInteger(5)); Asn1Der::encodeContextTag(priv, 1, Asn1Der::encodeInteger(21)); Asn1Der::encodeContextTag(priv, 3, Asn1Der::encodeEncryptedData(privKey.enctype, 0, privCipher)); std::vector<uint8_t> krbPriv; Asn1Der::encodeTLV(krbPriv, 0x75, Asn1Der::encodeSequence(priv)); return frameKpasswd(apRep, krbPriv); } /* * handleStandardRequest - Dispatch ASN.1 DER encoded Kerberos messages */ Loading Loading @@ -3171,6 +3423,22 @@ std::vector<uint8_t> Krb5KDC::handleStandardAsReq(const uint8_t *data, size_t le } } /* a ticket for the password-change service (kadmin/changepw, the * krbtgt key in AD) is what an expired password still gets */ bool changepw = req.sname.name == "kadmin" && req.sname.instance == "changepw"; /* pwdLastSet = 0: "must change password at next logon" */ if (!changepw) { if (auto who = ri->dir ? ri->dir->byGuid(uid) : std::nullopt) { auto *pls = who->get("pwdLastSet"); if (pls && !pls->empty() && pls->front() == "0") { std::cerr << "[KDC] AS-REQ: password of '" << who->str("sAMAccountName") << "' must be changed (pwdLastSet=0)" << std::endl; return buildStandardError(KDC_ERR_KEY_EXPIRED, "Password has expired", req.cname.realm, "krbtgt"); } } } /* Generate TGT session key */ Krb5SessionKey tgt_session_key = Krb5Crypto::generateSessionKey(selectedEtype); Loading Loading @@ -3251,7 +3519,8 @@ std::vector<uint8_t> Krb5KDC::handleStandardAsReq(const uint8_t *data, size_t le /* Build TGT */ Krb5Ticket tgt; tgt.tkt_vno = 5; tgt.sname = Krb5Principal(KRB5_NT_SRV_INST, req.cname.realm, "krbtgt", req.cname.realm); tgt.sname = changepw ? Krb5Principal(KRB5_NT_SRV_INST, req.cname.realm, "kadmin", "changepw") : Krb5Principal(KRB5_NT_SRV_INST, req.cname.realm, "krbtgt", req.cname.realm); tgt.enc_etype = selectedEtype; tgt.enc_kvno = 1; tgt.enc_part = enc_tkt_cipher; Loading Loading @@ -4542,7 +4811,7 @@ extern "C" { fenrirds::Plugin *fenrirds_plugin_create(fenrirds::AuthBackend &backend, const char *configJson) { std::string bindAddr = "0.0.0.0"; std::string bindAddr = "0.0.0.0", address; int port = 88; int maxconn = 128; Loading @@ -4555,11 +4824,15 @@ fenrirds::Plugin *fenrirds_plugin_create(fenrirds::AuthBackend &backend, if (jbind) bindAddr = json_object_get_string(jbind); if (jport) port = json_object_get_int(jport); if (jmax) maxconn = json_object_get_int(jmax); if (auto *ja = json_object_object_get(jobj, "dns_address")) address = json_object_get_string(ja); json_object_put(jobj); } } return new fenrirds::Krb5KDC(backend, bindAddr, port, maxconn); auto *kdc = new fenrirds::Krb5KDC(backend, bindAddr, port, maxconn); if (!address.empty()) kdc->setLocalAddress(address); return kdc; } void fenrirds_plugin_destroy(fenrirds::Plugin *p) { Loading
plugin/krb5/krb5.h +14 −1 Changes for plugin/krb5/krb5.h: 14 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -89,6 +89,7 @@ namespace fenrirds { KDC_ERR_NEVER_VALID = 11, KDC_ERR_POLICY = 12, KDC_ERR_BADOPTION = 13, KDC_ERR_KEY_EXPIRED = 23, KDC_ERR_PREAUTH_FAILED = 24, KDC_ERR_PREAUTH_REQUIRED = 25, KDC_ERR_WRONG_REALM = 68, Loading Loading @@ -144,6 +145,7 @@ namespace fenrirds { KRB5_KU_TGS_REP_ENC_PART = 8, /* TGS-REP encrypted part */ KRB5_KU_TGS_REP_ENC_PART_SUB = 9, /* TGS-REP enc part (subkey) */ KRB5_KU_AP_REP_ENC_PART = 12, /* AP-REP encrypted part */ KRB5_KU_KRB_PRIV_ENC_PART = 13, /* KRB-PRIV encrypted part (kpasswd) */ KRB5_KU_TICKET = 2 /* Ticket encrypted part */ }; Loading Loading @@ -770,6 +772,14 @@ namespace fenrirds { private: /* Resolve the realm from a request and return the RealmInfo, or nullptr */ RealmInfo *resolveRealm(const std::string &realm); /* kpasswd (RFC 3244, port 464): change/set password; localIp * (host order) is our address for the KRB-PRIV s-address */ std::vector<uint8_t> handleKpasswd(const uint8_t *data, size_t len, uint32_t localIp); static bool isKpasswd(const uint8_t *data, size_t len); public: /* the DC's IPv4 address (DNS/ADDRESS) for UDP kpasswd replies */ void setLocalAddress(const std::string &ipv4); private: void canonicalizeRealms(Krb5KdcReq &req); /* Standard Kerberos protocol (ASN.1 DER / RFC 4120) handlers */ Loading Loading @@ -829,8 +839,11 @@ namespace fenrirds { * Windows sends AS-REQ over UDP first. Served in its own thread. */ netplus::udp *_Udp = nullptr; std::thread _UdpThread; std::thread _KpwThread; netplus::udp *_UdpKpasswd = nullptr; uint32_t _LocalIp = 0; /* DC address (host order), UDP kpasswd replies */ std::atomic<bool> _Running{false}; void udpLoop(); void udpLoop(netplus::udp *sock); }; /*************************************************************************** Loading