Loading src/rpc/lsarpc.cpp +342 −1 Changes for src/rpc/lsarpc.cpp: 342 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -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. */ Loading @@ -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> &) { Loading Loading @@ -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 */ Loading tests/proto/test_lsarpc.cpp +119 −0 Changes for tests/proto/test_lsarpc.cpp: 119 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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 Loading
src/rpc/lsarpc.cpp +342 −1 Changes for src/rpc/lsarpc.cpp: 342 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -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. */ Loading @@ -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> &) { Loading Loading @@ -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 */ Loading
tests/proto/test_lsarpc.cpp +119 −0 Changes for tests/proto/test_lsarpc.cpp: 119 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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