Commit d13c9cdb authored by jan.koester's avatar jan.koester
Browse files

test

parent b8437558
Loading
Loading
Loading
Loading
Loading
+151 −3
Changes for plugin/ad/ad.cpp: 151 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@

#include "ad.h"
#include "crypto/gss.h"
#include "crypto/ntlm.h"

#include <sys/socket.h>
#include <netinet/in.h>
@@ -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) {
@@ -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())
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
}

+6 −0
Changes for plugin/ad/ad.h: 6 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -43,6 +43,7 @@

#include "backend.h"
#include "plugin.h"
#include "crypto/ntlm.h"
#include "dsdb/directory.h"
#include "dsdb/schema.h"

@@ -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;
+34 −0
Changes for plugin/krb5/krb5.cpp: 34 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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);

+2 −0
Changes for plugin/krb5/krb5.h: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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();
+82 −3
Changes for plugin/smb/smb.cpp: 82 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -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"
@@ -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};
}

/*****************************************************************************
@@ -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);
@@ -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