Commit 06a67019 authored by jan.koester's avatar jan.koester
Browse files

lsa lookp

parent de7f1a0c
Loading
Loading
Loading
Loading
Loading
+342 −1
Changes for src/rpc/lsarpc.cpp: 342 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -43,7 +43,17 @@ namespace {
    /* lsarpc UUID 12345778-1234-abcd-ef00-0123456789ab (NDR LE field order) */
    const uint8_t LSA_UUID[16] = {0x78,0x57,0x34,0x12,0x34,0x12,0xcd,0xab,0xef,0x00,0x01,0x23,0x45,0x67,0x89,0xab};

    enum { LSA_CLOSE = 0, LSA_QUERYINFOPOLICY = 7, LSA_OPENPOLICY2 = 44, LSA_QUERYINFOPOLICY2 = 46 };
    enum {
        LSA_CLOSE = 0, LSA_QUERYINFOPOLICY = 7, LSA_LOOKUPNAMES = 14, LSA_LOOKUPSIDS = 15,
        LSA_OPENPOLICY2 = 44, LSA_QUERYINFOPOLICY2 = 46, LSA_LOOKUPSIDS2 = 57, LSA_LOOKUPNAMES2 = 58,
        LSA_LOOKUPNAMES3 = 68, LSA_LOOKUPSIDS3 = 76, LSA_LOOKUPNAMES4 = 77,
    };

    /* lsa_SidType */
    enum : uint16_t {
        SID_USER = 1, SID_GROUP = 2, SID_DOMAIN = 3, SID_ALIAS = 4, SID_WKN_GROUP = 5, SID_UNKNOWN = 8,
    };
    constexpr uint32_t ST_SOME_NOT_MAPPED = 0x00000107, ST_NONE_MAPPED = 0xC0000073;

    /* an lsa_String / lsa_StringLarge: Length, Size, [ref]Buffer (conformant
     * varying wchar). lsa_String Size = Length; lsa_StringLarge Size = Length+2. */
@@ -66,6 +76,322 @@ namespace {
    }
}

namespace {
    /* One translated SID or name: its kind, name, SID and the referenced
     * domain (an index into the domain list, -1 for none). */
    struct Translation {
        uint16_t type = SID_UNKNOWN;
        std::string name;
        dsdb::Sid sid;
        int dom = -1;
    };

    /* The lsa_RefDomainList being built: each domain once */
    struct RefDomains {
        std::vector<std::pair<std::string, dsdb::Sid>> list;
        int add(const std::string &name, const dsdb::Sid &sid) {
            for (size_t i = 0; i < list.size(); ++i)
                if (list[i].second == sid)
                    return int(i);
            list.push_back({name, sid});
            return int(list.size() - 1);
        }
    };

    struct WellKnown {
        dsdb::Sid sid;
        const char *domain;   /* "" or NT AUTHORITY */
        const char *name;
    };
    const std::vector<WellKnown> &wellKnown() {
        static const std::vector<WellKnown> w = {
            {dsdb::wk::everyone(), "", "Everyone"},
            {dsdb::wk::creatorOwner(), "", "CREATOR OWNER"},
            {dsdb::Sid(3, {1}), "", "CREATOR GROUP"},
            {dsdb::wk::network(), "NT AUTHORITY", "NETWORK"},
            {dsdb::wk::anonymous(), "NT AUTHORITY", "ANONYMOUS LOGON"},
            {dsdb::wk::enterpriseDCs(), "NT AUTHORITY", "ENTERPRISE DOMAIN CONTROLLERS"},
            {dsdb::wk::self(), "NT AUTHORITY", "SELF"},
            {dsdb::wk::authenticatedUsers(), "NT AUTHORITY", "Authenticated Users"},
            {dsdb::wk::localSystem(), "NT AUTHORITY", "SYSTEM"},
            {dsdb::Sid(5, {4}), "NT AUTHORITY", "INTERACTIVE"},
            {dsdb::Sid(5, {6}), "NT AUTHORITY", "SERVICE"},
            {dsdb::Sid(5, {19}), "NT AUTHORITY", "LOCAL SERVICE"},
            {dsdb::Sid(5, {20}), "NT AUTHORITY", "NETWORK SERVICE"},
        };
        return w;
    }

    uint16_t kindOf(const dsdb::Entry &e) {
        if (e.isClass("user") || e.isClass("computer"))
            return SID_USER;
        if (e.isClass("group")) {
            uint32_t gt = uint32_t(dsdb::intValue(e.attrs, "groupType", 0));
            return gt & (dsdb::GROUP_DOMAIN_LOCAL | dsdb::GROUP_BUILTIN_LOCAL) ? SID_ALIAS : SID_GROUP;
        }
        return SID_UNKNOWN;
    }

    /* an account of the directory: the domain it is in (ours or BUILTIN) */
    void fromEntry(const dsdb::Entry &e, const DomainInfo &di, RefDomains &doms, Translation &t) {
        t.type = kindOf(e);
        t.name = e.str("sAMAccountName");
        t.sid = e.objectSid;
        if (e.objectSid.inDomain(dsdb::wk::builtin()))
            t.dom = doms.add("BUILTIN", dsdb::wk::builtin());
        else
            t.dom = doms.add(di.netbios, di.sid);
    }

    /* the SIDs of the request; empty SIDs for NULL entries */
    std::vector<dsdb::Sid> pullSidArray(NdrPull &in) {
        std::vector<dsdb::Sid> out;
        uint32_t num = in.u32();
        if (!in.ref())
            return out;
        uint32_t max = in.u32();
        if (max != num || max > 20480)
            throw NdrError("bad SID array");
        std::vector<uint32_t> ptrs(max);
        for (auto &p : ptrs)
            p = in.ref();
        for (uint32_t p : ptrs)
            out.push_back(p ? in.sid() : dsdb::Sid());
        return out;
    }

    /* [in,out] names / sids arrays in a request: clients send them empty */
    void skipEmptyInOut(NdrPull &in) {
        in.u32();
        if (in.ref())
            throw NdrError("non-empty in,out translation array");
    }

    void pushRefDomains(NdrPush &p, const RefDomains &doms) {
        p.ref(true);                                  /* *domains */
        p.u32(uint32_t(doms.list.size()));
        p.ref(!doms.list.empty());
        p.u32(32);                                    /* max_size */
        if (doms.list.empty())
            return;
        p.u32(uint32_t(doms.list.size()));            /* conformance */
        for (auto &d : doms.list) {
            pushLsaString(p, d.first, true);
            p.ref(true);
        }
        for (auto &d : doms.list) {
            pushLsaStringBuffer(p, d.first, true);
            p.sid(d.second);
        }
    }

    uint32_t mappedStatus(const std::vector<Translation> &t, uint32_t &mapped) {
        mapped = 0;
        for (auto &x : t)
            if (x.type != SID_UNKNOWN)
                ++mapped;
        return mapped == t.size() ? 0 : mapped ? ST_SOME_NOT_MAPPED : ST_NONE_MAPPED;
    }

    /* SID -> name */
    Translation lookupSid(const dsdb::Sid &sid, const DomainInfo &di, dsdb::Directory *dir, RefDomains &doms) {
        Translation t;
        t.sid = sid;
        if (sid.empty())
            return t;
        for (auto &w : wellKnown())
            if (w.sid == sid) {
                t.type = SID_WKN_GROUP;
                t.name = w.name;
                t.dom = doms.add(w.domain, sid.domain());
                return t;
            }
        if (di.ok && sid == di.sid) {
            t.type = SID_DOMAIN;
            t.name = di.netbios;
            t.dom = doms.add(di.netbios, di.sid);
            return t;
        }
        if (sid == dsdb::wk::builtin()) {
            t.type = SID_DOMAIN;
            t.name = "BUILTIN";
            t.dom = doms.add("BUILTIN", sid);
            return t;
        }
        if (dir && (sid.inDomain(di.sid) || sid.inDomain(dsdb::wk::builtin())))
            if (auto e = dir->bySid(sid))
                fromEntry(*e, di, doms, t);
        return t;
    }

    /* name ([DOMAIN\]name, name@dns) -> SID */
    Translation lookupName(const std::string &full, const DomainInfo &di, dsdb::Directory *dir, RefDomains &doms) {
        Translation t;
        std::string dom, name = full;
        size_t bs = full.find('\\');
        if (bs != std::string::npos) {
            dom = full.substr(0, bs);
            name = full.substr(bs + 1);
        } else if (size_t at = full.rfind('@'); at != std::string::npos && di.ok &&
                   dsdb::iequals(full.substr(at + 1), di.dnsName)) {
            dom = di.netbios;
            name = full.substr(0, at);
        }
        for (auto &w : wellKnown())
            if (dsdb::iequals(name, w.name) && (dom.empty() || dsdb::iequals(dom, w.domain))) {
                t.type = SID_WKN_GROUP;
                t.name = w.name;
                t.sid = w.sid;
                t.dom = doms.add(w.domain, w.sid.domain());
                return t;
            }
        bool ours = dom.empty() || (di.ok && (dsdb::iequals(dom, di.netbios) || dsdb::iequals(dom, di.dnsName)));
        bool builtin = dsdb::iequals(dom, "BUILTIN");
        if (!dir || !(ours || builtin))
            return t;
        if (di.ok && dom.empty() && (dsdb::iequals(name, di.netbios) || dsdb::iequals(name, di.dnsName))) {
            t.type = SID_DOMAIN;
            t.name = di.netbios;
            t.sid = di.sid;
            t.dom = doms.add(di.netbios, di.sid);
            return t;
        }
        auto e = dir->bySamAccountName(name);
        if (!e || e->objectSid.empty())
            return t;
        /* BUILTIN\x only for builtin aliases, our domain only for its accounts */
        bool isBuiltin = e->objectSid.inDomain(dsdb::wk::builtin());
        if ((builtin && !isBuiltin) || (!dom.empty() && ours && isBuiltin))
            return t;
        fromEntry(*e, di, doms, t);
        return t;
    }

    /* the directory of the primary domain, for the length of a call */
    template <class F> void withDirectory(const DomainInfo &di, F f) {
        AuthBackend *bck = di.ok ? DomainRegistry::instance().get(di.domainId) : nullptr;
        if (!bck) {
            f(nullptr);
            return;
        }
        dsdb::DomainMeta meta;
        meta.dnsName = di.dnsName;
        meta.netbios = di.netbios;
        meta.baseDn = di.baseDn;
        meta.realm = dsdb::upper(di.dnsName);
        meta.sid = di.sid;
        dsdb::Directory dir(*bck, meta);
        f(&dir);
    }

    DispatchResult lookupSids(uint16_t opnum, const std::vector<uint8_t> &stub) {
        DispatchResult r;
        NdrPull in(stub);
        if (opnum != LSA_LOOKUPSIDS3)
            in.handle();
        std::vector<dsdb::Sid> sids = pullSidArray(in);
        skipEmptyInOut(in);
        /* level, count (, lookup_options, client_revision): every level
         * gets the same answer here -- we are the only domain */

        DomainInfo di = primaryDomain();
        RefDomains doms;
        std::vector<Translation> out;
        withDirectory(di, [&](dsdb::Directory *dir) {
            for (auto &s : sids)
                out.push_back(lookupSid(s, di, dir, doms));
        });

        bool v2 = opnum != LSA_LOOKUPSIDS;   /* lsa_TranslatedName2: + flags */
        NdrPush p;
        pushRefDomains(p, doms);
        p.u32(uint32_t(out.size()));
        p.ref(!out.empty());
        if (!out.empty()) {
            p.u32(uint32_t(out.size()));
            for (auto &t : out) {
                p.u16(t.type);
                p.align(4);
                pushLsaString(p, t.name, false);
                p.u32(t.dom < 0 ? 0xffffffffu : uint32_t(t.dom));
                if (v2)
                    p.u32(0);
            }
            for (auto &t : out)
                pushLsaStringBuffer(p, t.name, false);
        }
        uint32_t mapped;
        r.status = mappedStatus(out, mapped);
        p.u32(mapped);
        p.u32(r.status);
        r.stub = p.buf;
        return r;
    }

    DispatchResult lookupNames(uint16_t opnum, const std::vector<uint8_t> &stub) {
        DispatchResult r;
        NdrPull in(stub);
        if (opnum != LSA_LOOKUPNAMES4)
            in.handle();
        uint32_t num = in.u32();
        uint32_t max = in.u32();
        if (max != num || max > 1000)
            throw NdrError("bad name array");
        std::vector<uint32_t> ptrs(num);
        for (auto &pt : ptrs) {
            in.u16();
            in.u16();
            pt = in.ref();
        }
        std::vector<std::string> names;
        for (uint32_t pt : ptrs)
            names.push_back(pt ? in.unistrBuffer() : std::string());
        skipEmptyInOut(in);

        DomainInfo di = primaryDomain();
        RefDomains doms;
        std::vector<Translation> out;
        withDirectory(di, [&](dsdb::Directory *dir) {
            for (auto &n : names)
                out.push_back(lookupName(n, di, dir, doms));
        });

        NdrPush p;
        pushRefDomains(p, doms);
        p.u32(uint32_t(out.size()));
        p.ref(!out.empty());
        if (!out.empty()) {
            p.u32(uint32_t(out.size()));
            bool v3 = opnum == LSA_LOOKUPNAMES3 || opnum == LSA_LOOKUPNAMES4;
            for (auto &t : out) {
                p.u16(t.type);
                p.align(4);
                if (v3) {
                    /* lsa_TranslatedSid3: the whole SID */
                    p.ref(t.type != SID_UNKNOWN);
                } else {
                    /* lsa_TranslatedSid(2): the RID in the referenced domain */
                    p.u32(t.type == SID_UNKNOWN || t.type == SID_DOMAIN || t.sid.sub.empty() ? 0xffffffffu
                                                                                             : t.sid.rid());
                }
                p.u32(t.dom < 0 ? 0xffffffffu : uint32_t(t.dom));
                if (opnum != LSA_LOOKUPNAMES)
                    p.u32(0);
            }
            if (v3)
                for (auto &t : out)
                    if (t.type != SID_UNKNOWN)
                        p.sid(t.sid);
        }
        uint32_t mapped;
        r.status = mappedStatus(out, mapped);
        p.u32(mapped);
        p.u32(r.status);
        r.stub = p.buf;
        return r;
    }
}

bool isLsarpc(const uint8_t uuid[16]) { return std::memcmp(uuid, LSA_UUID, 16) == 0; }

DispatchResult lsarpcDispatch(uint16_t opnum, const std::vector<uint8_t> &stub, const std::vector<uint8_t> &) {
@@ -134,6 +460,21 @@ DispatchResult lsarpcDispatch(uint16_t opnum, const std::vector<uint8_t> &stub,
        r.stub = p.buf;
        return r;
    }
    case LSA_LOOKUPSIDS:
    case LSA_LOOKUPSIDS2:
    case LSA_LOOKUPSIDS3:
    case LSA_LOOKUPNAMES:
    case LSA_LOOKUPNAMES2:
    case LSA_LOOKUPNAMES3:
    case LSA_LOOKUPNAMES4:
        try {
            bool sids = opnum == LSA_LOOKUPSIDS || opnum == LSA_LOOKUPSIDS2 || opnum == LSA_LOOKUPSIDS3;
            return sids ? lookupSids(opnum, stub) : lookupNames(opnum, stub);
        } catch (const NdrError &) {
            r.fault = true;
            r.faultCode = 0x000006f7;  /* RPC_X_BAD_STUB_DATA */
            return r;
        }
    default:
        r.fault = true;
        r.faultCode = 0x1C010002;  /* nca_op_rng_error */
+119 −0
Changes for tests/proto/test_lsarpc.cpp: 119 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -94,4 +94,123 @@ TEST(lsarpc_open_and_query) {
    unlink(path.c_str()); unlink((path + ".lock").c_str()); unlink((path + ".dom").c_str());
}

/* the lsa_RefDomainList of a lookup answer: names and SIDs */
static std::vector<std::pair<std::string, dsdb::Sid>> pullDomains(NdrPull &r) {
    std::vector<std::pair<std::string, dsdb::Sid>> out;
    CHECK(r.ref() != 0);
    uint32_t count = r.u32();
    uint32_t ptr = r.ref();
    r.u32();                                   /* max_size */
    if (!ptr) return out;
    CHECK_EQ(r.u32(), count);
    std::vector<uint32_t> namePtr(count);
    for (uint32_t i = 0; i < count; ++i) { uint16_t l, m; namePtr[i] = r.unistrHeader(l, m); r.ref(); }
    for (uint32_t i = 0; i < count; ++i) {
        std::string n = namePtr[i] ? r.unistrBuffer() : std::string();
        out.push_back({n, r.sid()});
    }
    return out;
}

struct Name { uint16_t type; std::string name; uint32_t dom; };

TEST(lsarpc_lookup_sids_and_names) {
    std::string path = "/build/fds-lsa2-" + std::to_string(getpid()) + ".db";
    setup(path);
    rpc::DomainInfo di = primaryDomain();
    dsdb::Sid admin = di.sid.withRid(500), users = di.sid.withRid(513);
    dsdb::Sid unknown = dsdb::Sid(22, {1, 1000});
    std::vector<dsdb::Sid> sids = {admin, dsdb::wk::everyone(), unknown, dsdb::wk::builtinAlias(544), users};

    /* LookupSids2: handle, lsa_SidArray, empty names, level, count, options, revision */
    NdrPush q;
    PolicyHandle h; q.handle(h);
    q.u32(uint32_t(sids.size())); q.ref(true);
    q.u32(uint32_t(sids.size()));
    for (size_t i = 0; i < sids.size(); ++i) q.ref(true);
    for (auto &x : sids) q.sid(x);
    q.u32(0); q.ref(false);                      /* names */
    q.u16(1); q.u32(0); q.u32(0); q.u32(2);
    auto a = lsarpcDispatch(57, q.buf);
    CHECK(!a.fault);
    NdrPull r(a.stub);
    auto doms = pullDomains(r);
    CHECK_EQ(r.u32(), uint32_t(sids.size()));
    CHECK(r.ref() != 0);
    CHECK_EQ(r.u32(), uint32_t(sids.size()));
    std::vector<Name> names;
    std::vector<uint32_t> ptrs;
    for (size_t i = 0; i < sids.size(); ++i) {
        Name n; n.type = r.u16(); r.align(4);
        uint16_t l, m; ptrs.push_back(r.unistrHeader(l, m));
        n.dom = r.u32(); r.u32();               /* sid_index, flags */
        names.push_back(n);
    }
    for (size_t i = 0; i < sids.size(); ++i) names[i].name = ptrs[i] ? r.unistrBuffer() : std::string();
    CHECK_EQ(r.u32(), uint32_t(4));             /* mapped */
    CHECK_EQ(r.u32(), uint32_t(0x00000107));    /* STATUS_SOME_NOT_MAPPED */
    CHECK_EQ(names[0].type, uint16_t(1));
    CHECK_EQ(names[0].name, std::string("Administrator"));
    CHECK_EQ(doms[names[0].dom].first, std::string("EXAMPLE"));
    CHECK(doms[names[0].dom].second == di.sid);
    CHECK_EQ(names[1].type, uint16_t(5));
    CHECK_EQ(names[1].name, std::string("Everyone"));
    CHECK_EQ(names[2].type, uint16_t(8));       /* S-1-22-1-1000: a file server's, not ours */
    CHECK_EQ(names[2].dom, uint32_t(0xffffffff));
    CHECK_EQ(names[3].type, uint16_t(4));
    CHECK_EQ(names[3].name, std::string("Administrators"));
    CHECK_EQ(doms[names[3].dom].first, std::string("BUILTIN"));
    CHECK_EQ(names[4].type, uint16_t(2));
    CHECK_EQ(names[4].name, std::string("Domain Users"));
    CHECK_EQ(r.remaining(), size_t(0));

    /* LookupNames3: the other way round, whole SIDs back */
    std::vector<std::string> in = {"Administrator", "EXAMPLE\\Domain Users", "BUILTIN\\Administrators",
                                   "administrator@example.com", "Everyone", "nobody", "BUILTIN\\Administrator"};
    NdrPush n3;
    n3.handle(h);
    n3.u32(uint32_t(in.size()));
    n3.u32(uint32_t(in.size()));
    for (auto &x : in) { n3.u16(uint16_t(x.size() * 2)); n3.u16(uint16_t(x.size() * 2)); n3.ref(true); }
    for (auto &x : in) n3.unistrBuffer(x);
    n3.u32(0); n3.ref(false);                    /* sids */
    n3.u16(1); n3.u32(0); n3.u32(0); n3.u32(2);
    auto b = lsarpcDispatch(68, n3.buf);
    CHECK(!b.fault);
    NdrPull rb(b.stub);
    auto doms2 = pullDomains(rb);
    CHECK_EQ(rb.u32(), uint32_t(in.size()));
    CHECK(rb.ref() != 0);
    CHECK_EQ(rb.u32(), uint32_t(in.size()));
    std::vector<uint16_t> types; std::vector<uint32_t> sp;
    for (size_t i = 0; i < in.size(); ++i) { types.push_back(rb.u16()); rb.align(4); sp.push_back(rb.ref()); rb.u32(); rb.u32(); }
    std::vector<dsdb::Sid> got;
    for (size_t i = 0; i < in.size(); ++i) got.push_back(sp[i] ? rb.sid() : dsdb::Sid());
    CHECK_EQ(rb.u32(), uint32_t(5));
    CHECK_EQ(rb.u32(), uint32_t(0x00000107));
    CHECK(got[0] == admin && types[0] == 1);
    CHECK(got[1] == users && types[1] == 2);
    CHECK(got[2] == dsdb::wk::builtinAlias(544) && types[2] == 4);
    CHECK(got[3] == admin);
    CHECK(got[4] == dsdb::wk::everyone() && types[4] == 5);
    CHECK_EQ(types[5], uint16_t(8));
    CHECK_EQ(types[6], uint16_t(8));            /* BUILTIN\\Administrator: no such alias */
    CHECK_EQ(rb.remaining(), size_t(0));
    (void)doms2;

    /* LookupSids3 (no handle) answers the same */
    NdrPush q3;
    q3.u32(1); q3.ref(true); q3.u32(1); q3.ref(true); q3.sid(admin);
    q3.u32(0); q3.ref(false); q3.u16(1); q3.u32(0); q3.u32(0); q3.u32(2);
    auto c = lsarpcDispatch(76, q3.buf);
    CHECK(!c.fault);
    CHECK_EQ(c.status, uint32_t(0));

    /* garbage: a fault, not a crash */
    auto g = lsarpcDispatch(57, std::vector<uint8_t>(7, 0xff));
    CHECK(g.fault);

    unlink(path.c_str()); unlink((path + ".lock").c_str()); unlink((path + ".dom").c_str());
}

CHECK_MAIN