diff --git a/modules/engine/attributes.go b/modules/engine/attributes.go index 7ea7571..334fe0e 100644 --- a/modules/engine/attributes.go +++ b/modules/engine/attributes.go @@ -103,6 +103,29 @@ var ( MetaLAPSInstalled = NewAttribute("_haslaps") ) +func init() { + AddMergeApprover("Merge SIDs", func(a, b *Object) (*Object, error) { + asid := a.SID() + bsid := b.SID() + if asid.IsBlank() || bsid.IsBlank() { + return nil, nil + } + if asid != bsid { + return nil, ErrDontMerge + } + if asid.Component(2) == 21 { + return nil, nil // Merge, these should be universally mappable !? + } + asource := a.OneAttr(DataSource) + bsource := b.OneAttr(DataSource) + if CompareAttributeValues(asource, bsource) { + // Stuff from GPOs can have non universal SIDs but should still be mapped + return nil, nil + } + return nil, ErrDontMerge + }) +} + type Attribute uint16 type AttributePair struct { diff --git a/modules/engine/attributevalue.go b/modules/engine/attributevalue.go index e40146a..607a7b1 100644 --- a/modules/engine/attributevalue.go +++ b/modules/engine/attributevalue.go @@ -55,6 +55,9 @@ func CompareAttributeValues(a, b AttributeValue) bool { } default: // Fallback + if a == nil || b == nil { + return a == b + } return a.String() == b.String() } diff --git a/modules/engine/objects.go b/modules/engine/objects.go index f4fe426..e13fdbd 100644 --- a/modules/engine/objects.go +++ b/modules/engine/objects.go @@ -691,52 +691,31 @@ func (os *Objects) FindOrAddAdjacentSID(s windowssecurity.SID, r *Object) *Objec result, _ := os.FindMultiOrAdd(ObjectSid, AttributeValueSID(s), func() *Object { no := NewObject( ObjectSid, AttributeValueSID(s), - DataLoader, "FindOrAddAdjacentSID", - IgnoreBlanks, - DomainContext, r.Attr(DomainContext), ) - if !r.SID().IsNull() { - if r.SID().StripRID() == s.StripRID() { - // Same domain ... hmm! - } else { - // Other domain, then it's a foreign principal - no.SetFlex(ObjectCategorySimple, "Foreign-Security-Principal") - if domainContext := r.OneAttrString(DomainContext); domainContext != "" { - no.SetFlex(DistinguishedName, "CN="+s.String()+",CN=ForeignSecurityPrincipals,"+domainContext) - } - } - } return no }) return result.First() - default: - if r.HasAttr(DomainContext) { - // From outside, we need to find the domain part - if o, found := os.FindTwoMulti(ObjectSid, AttributeValueSID(s), DomainContext, r.OneAttr(DomainContext)); found { - return o.First() - } + } + + if r.HasAttr(DomainContext) { + // From outside, we need to find the domain part + if o, found := os.FindTwoMulti(ObjectSid, AttributeValueSID(s), DomainContext, r.OneAttr(DomainContext)); found { + return o.First() } - // From inside same source, that is easy - if r.HasAttr(DataSource) { - if o, found := os.FindTwoMulti(ObjectSid, AttributeValueSID(s), DataSource, r.OneAttr(DataSource)); found { - return o.First() - } + } + // From inside same source, that is easy + if r.HasAttr(DataSource) { + if o, found := os.FindTwoMulti(ObjectSid, AttributeValueSID(s), DataSource, r.OneAttr(DataSource)); found { + return o.First() } - } // Not found, we have write lock so create it - no := NewObject(ObjectSid, AttributeValueSID(s)) - - if s.Component(2) != 21 { - no.SetFlex( - IgnoreBlanks, - DomainContext, r.Attr(DomainContext), - DataSource, r.Attr(DataSource), - ) - } - - os.Add(no) + no, _ := os.FindOrAdd(ObjectSid, AttributeValueSID(s), + IgnoreBlanks, + DomainContext, r.Attr(DomainContext), + DataSource, r.Attr(DataSource), + ) return no } diff --git a/modules/engine/processing.go b/modules/engine/processing.go index 50594e2..a296ffd 100644 --- a/modules/engine/processing.go +++ b/modules/engine/processing.go @@ -137,16 +137,27 @@ func Merge(aos []*Objects) (*Objects, error) { // We're grabbing the index directly for faster processing here dnindex := globalobjects.GetIndex(DistinguishedName) - // Just add these. they have a DataSource so we're not merging them EXCEPT for ones with a DistinguishedName collition FML + mergeon := getMergeAttributes() + + // Just add these. they have a DataSource so we're not merging them EXCEPT for ones with a DistinguishedName collision FML sourcemap.Range(func(us string, usao sourceinfo) bool { if us == "" { return true // continue - not these, we'll try to merge at the very end } usao.shard.Iterate(func(addobject *Object) bool { pb.Add(1) - // Here we'll deduplicate DNs, because sometimes schema and config context slips in twice + // Here we'll deduplicate DNs, because sometimes schema and config context slips in twice ... + aosid := addobject.SID() + if !aosid.IsBlank() && aosid.Component(2) == 21 { + // Always merge these, they might belong elsewhere + globalobjects.AddMerge(mergeon, addobject) + return true + } if dn := addobject.OneAttr(DistinguishedName); dn != nil { - if existing, exists := dnindex.Lookup(AttributeValueToIndex(dn)); exists { + // UNLESS it's a predefined one with an objectSID, in that case done merge them at all WTF not a huge fan of this design, Microsoft + if !aosid.IsBlank() && aosid.Component(2) != 21 { + addobject.SetFlex(DistinguishedName, "mutated="+addobject.OneAttrString(DataSource)+","+dn.String()) + } else if existing, exists := dnindex.Lookup(AttributeValueToIndex(dn)); exists { existing.First().AbsorbEx(addobject, true) return true } @@ -159,7 +170,6 @@ func Merge(aos []*Objects) (*Objects, error) { nodatasource, _ := sourcemap.Load("") var i int - mergeon := getMergeAttributes() nodatasource.shard.Iterate(func(addobject *Object) bool { pb.Add(1) // Here we'll deduplicate DNs, because sometimes schema and config context slips in twice diff --git a/modules/integrations/activedirectory/analyze/adloader.go b/modules/integrations/activedirectory/analyze/adloader.go index 1dcb002..03e7097 100644 --- a/modules/integrations/activedirectory/analyze/adloader.go +++ b/modules/integrations/activedirectory/analyze/adloader.go @@ -103,6 +103,10 @@ func (ld *ADLoader) Init() error { continue // skip deleted object } + if strings.Contains(o.DN(), ",CN=ForeignSecurityPrincipals,DC=") { + continue // skip all foreign security principals + } + if !o.HasAttr(engine.ObjectClass) { if ld.warnhardened { if strings.Contains(o.DN(), ",CN=MicrosoftDNS,") { diff --git a/modules/integrations/activedirectory/analyze/analyze-ad.go b/modules/integrations/activedirectory/analyze/analyze-ad.go index aea713d..9b6f23b 100644 --- a/modules/integrations/activedirectory/analyze/analyze-ad.go +++ b/modules/integrations/activedirectory/analyze/analyze-ad.go @@ -34,6 +34,7 @@ var ( AttributeProfilePathGUID, _ = uuid.FromString("{bf967a05-0de6-11d0-a285-00aa003049e2}") AttributeScriptPathGUID, _ = uuid.FromString("{bf9679a8-0de6-11d0-a285-00aa003049e2}") AttributeMSDSManagedPasswordId, _ = uuid.FromString("{0e78295a-c6d3-0a40-b491-d62251ffa0a6}") + AttributeUserAccountControlGUID, _ = uuid.FromString("{bf967a68-0de6-11d0-a285-00aa003049e2}") ExtendedRightCertificateEnroll, _ = uuid.FromString("{0e10c968-78fb-11d2-90d4-00c04f79dc55}") ExtendedRightCertificateAutoEnroll, _ = uuid.FromString("{a05b8cc2-17bc-4802-a710-e7c15ab866a2}") @@ -1102,7 +1103,7 @@ func init() { } // Crude special handling for Everyone and Authenticated Users - if object.Type() == engine.ObjectTypeUser || object.Type() == engine.ObjectTypeComputer || object.Type() == engine.ObjectTypeManagedServiceAccount || object.Type() == engine.ObjectTypeForeignSecurityPrincipal || object.Type() == engine.ObjectTypeGroupManagedServiceAccount { + if object.Type() == engine.ObjectTypeUser || object.Type() == engine.ObjectTypeComputer || object.Type() == engine.ObjectTypeManagedServiceAccount || object.Type() == engine.ObjectTypeGroupManagedServiceAccount { object.EdgeTo(authenticatedusers, activedirectory.EdgeMemberOfGroup) } authenticatedusers.EdgeTo(everyone, activedirectory.EdgeMemberOfGroup) @@ -1310,50 +1311,50 @@ func init() { engine.BeforeMerge, ) - Loader.AddProcessor(func(ao *engine.Objects) { - // Find all the DomainDNS objects, and find the domain object - domains := make(map[string]windowssecurity.SID) - - domaindnsobjects, found := ao.FindMulti(engine.ObjectClass, engine.AttributeValueString("domainDNS")) - - if !found { - ui.Error().Msg("Could not find any domainDNS objects") - } - - domaindnsobjects.Iterate(func(domaindnsobject *engine.Object) bool { - domainSID, sidok := domaindnsobject.OneAttrRaw(activedirectory.ObjectSid).(windowssecurity.SID) - dn := domaindnsobject.OneAttrString(activedirectory.DistinguishedName) - if sidok { - domains[dn] = domainSID - } - return true - }) - - ao.Iterate(func(o *engine.Object) bool { - if o.HasAttr(engine.ObjectSid) && o.SID().Component(2) == 21 && !o.HasAttr(engine.DistinguishedName) && o.HasAttr(engine.DomainContext) { - // An unknown SID, is it ours or from another domain? - ourDomainDN := o.OneAttrString(engine.DomainContext) - ourDomainSid, domainfound := domains[ourDomainDN] - if !domainfound { - return true - } - - if o.SID().StripRID() == ourDomainSid { - // ui.Debug().Msgf("Found a 'dangling' local SID object %v. This is either a SID from a deleted object (most likely) or hardened objects that are not readable with the account used to dump data.", o.SID()) - } else { - // ui.Debug().Msgf("Found a 'lost' foreign SID object %v, adding it as a synthetic Foreign-Security-Principal", o.SID()) - o.SetFlex( - engine.DistinguishedName, engine.AttributeValueString(o.SID().String()+",CN=ForeignSecurityPrincipals,"+ourDomainDN), - engine.ObjectCategorySimple, "Foreign-Security-Principal", - engine.DataLoader, "Autogenerated", - ) - } - } - return true - }) - }, - "Creation of synthetic Foreign-Security-Principal objects", - engine.AfterMergeLow) + // Loader.AddProcessor(func(ao *engine.Objects) { + // // Find all the DomainDNS objects, and find the domain object + // domains := make(map[string]windowssecurity.SID) + + // domaindnsobjects, found := ao.FindMulti(engine.ObjectClass, engine.AttributeValueString("domainDNS")) + + // if !found { + // ui.Error().Msg("Could not find any domainDNS objects") + // } + + // domaindnsobjects.Iterate(func(domaindnsobject *engine.Object) bool { + // domainSID, sidok := domaindnsobject.OneAttrRaw(activedirectory.ObjectSid).(windowssecurity.SID) + // dn := domaindnsobject.OneAttrString(activedirectory.DistinguishedName) + // if sidok { + // domains[dn] = domainSID + // } + // return true + // }) + + // ao.Iterate(func(o *engine.Object) bool { + // if o.HasAttr(engine.ObjectSid) && o.SID().Component(2) == 21 && !o.HasAttr(engine.DistinguishedName) && o.HasAttr(engine.DomainContext) { + // // An unknown SID, is it ours or from another domain? + // ourDomainDN := o.OneAttrString(engine.DomainContext) + // ourDomainSid, domainfound := domains[ourDomainDN] + // if !domainfound { + // return true + // } + + // if o.SID().StripRID() == ourDomainSid { + // // ui.Debug().Msgf("Found a 'dangling' local SID object %v. This is either a SID from a deleted object (most likely) or hardened objects that are not readable with the account used to dump data.", o.SID()) + // } else { + // // ui.Debug().Msgf("Found a 'lost' foreign SID object %v, adding it as a synthetic Foreign-Security-Principal", o.SID()) + // o.SetFlex( + // engine.DistinguishedName, engine.AttributeValueString(o.SID().String()+",CN=ForeignSecurityPrincipals,"+ourDomainDN), + // engine.ObjectCategorySimple, "Foreign-Security-Principal", + // engine.DataLoader, "Autogenerated", + // ) + // } + // } + // return true + // }) + // }, + // "Creation of synthetic Foreign-Security-Principal objects", + // engine.AfterMergeLow) Loader.AddProcessor(func(ao *engine.Objects) { ao.Iterate(func(machine *engine.Object) bool { @@ -1559,21 +1560,18 @@ func init() { memberobject, found := ao.Find(engine.DistinguishedName, member) if !found { var sid engine.AttributeValueSID - var category string if stringsid, _, found := strings.Cut(member.String(), ",CN=ForeignSecurityPrincipals,"); found { // We can figure out what the SID is if c, err := windowssecurity.ParseStringSID(stringsid); err == nil { sid = engine.AttributeValueSID(c) - category = "Foreign-Security-Principal" + member = nil } - ui.Info().Msgf("Missing Foreign-Security-Principal: %v is a member of %v, which is not found - adding enhanced synthetic group", object.DN(), member) } else { ui.Warn().Msgf("Possible hardening? %v is a member of %v, which is not found - adding synthetic group. Your analysis will be degraded, try dumping with Domain Admin rights.", object.DN(), member) } memberobject = engine.NewObject( engine.IgnoreBlanks, engine.DistinguishedName, member, - engine.ObjectCategorySimple, category, engine.ObjectSid, sid, engine.DataLoader, "Autogenerated", ) @@ -1589,6 +1587,26 @@ func init() { engine.AfterMergeLow, ) + Loader.AddProcessor(func(ao *engine.Objects) { + ao.Iterate(func(o *engine.Object) bool { + // Only for containers and org units + if o.Type() != engine.ObjectTypeUser { + return true + } + + sd, err := o.SecurityDescriptor() + if err != nil { + return true + } + for index, acl := range sd.DACL.Entries { + if sd.DACL.IsObjectClassAccessAllowed(index, o, engine.RIGHT_DS_WRITE_PROPERTY, AttributeUserAccountControlGUID, ao) { + ao.FindOrAddAdjacentSID(acl.SID, o).EdgeTo(o, activedirectory.EdgeWriteUserAccountControl) + } + } + return true + }) + }, "Permissions that lets someone modify userAccountControl", engine.BeforeMergeFinal) + Loader.AddProcessor(func(ao *engine.Objects) { ao.IterateParallel(func(o *engine.Object) bool { // Object that is member of something @@ -1610,32 +1628,34 @@ func init() { engine.AfterMerge, ) - Loader.AddProcessor(func(ao *engine.Objects) { - ao.Filter(func(o *engine.Object) bool { - return o.Type() == engine.ObjectTypeForeignSecurityPrincipal - }).Iterate(func(foreign *engine.Object) bool { - sid := foreign.SID() - if sid.IsNull() { - ui.Error().Msgf("Found a foreign security principal with no SID %v", foreign.Label()) - return true - } - if sid.Component(2) == 21 { - if sources, found := ao.FindMulti(engine.ObjectSid, engine.AttributeValueSID(sid)); found { - sources.Iterate(func(source *engine.Object) bool { - if source.Type() != engine.ObjectTypeForeignSecurityPrincipal { - source.EdgeToEx(foreign, activedirectory.EdgeForeignIdentity, true) - } - return true - }) + /* + Loader.AddProcessor(func(ao *engine.Objects) { + ao.Filter(func(o *engine.Object) bool { + return o.Type() == engine.ObjectTypeForeignSecurityPrincipal + }).Iterate(func(foreign *engine.Object) bool { + sid := foreign.SID() + if sid.IsNull() { + ui.Error().Msgf("Found a foreign security principal with no SID %v", foreign.Label()) + return true } - } else { - ui.Warn().Msgf("Found a foreign security principal %v with an non type 21 SID %v", foreign.DN(), sid.String()) - } - return true - }) - }, "Link foreign security principals to their native objects", - engine.AfterMerge, - ) + if sid.Component(2) == 21 { + if sources, found := ao.FindMulti(engine.ObjectSid, engine.AttributeValueSID(sid)); found { + sources.Iterate(func(source *engine.Object) bool { + if source.Type() != engine.ObjectTypeForeignSecurityPrincipal { + source.EdgeToEx(foreign, activedirectory.EdgeForeignIdentity, true) + } + return true + }) + } + } else { + ui.Warn().Msgf("Found a foreign security principal %v with an non type 21 SID %v", foreign.DN(), sid.String()) + } + return true + }) + }, "Link foreign security principals to their native objects", + engine.AfterMerge, + ) + */ Loader.AddProcessor(func(ao *engine.Objects) { var warnlines int diff --git a/modules/integrations/activedirectory/analyze/knownsids.go b/modules/integrations/activedirectory/analyze/knownsids.go index 69bd725..e8efdbb 100644 --- a/modules/integrations/activedirectory/analyze/knownsids.go +++ b/modules/integrations/activedirectory/analyze/knownsids.go @@ -50,10 +50,7 @@ func FindWellKnown(ao *engine.Objects, s windowssecurity.SID) *engine.Object { results, _ := ao.FindMulti(engine.ObjectSid, engine.AttributeValueSID(s)) var result *engine.Object results.Iterate(func(o *engine.Object) bool { - if result == nil || result.Type() == engine.ObjectTypeForeignSecurityPrincipal { - // Prefer non-FSP object - result = o - } + result = o return true }) return result diff --git a/modules/integrations/activedirectory/pwns.go b/modules/integrations/activedirectory/pwns.go index ded2ead..0525330 100644 --- a/modules/integrations/activedirectory/pwns.go +++ b/modules/integrations/activedirectory/pwns.go @@ -34,20 +34,46 @@ var ( } return 50 }) - EdgeWriteAllowedToAct = engine.NewEdge("WriteAllowedToAct") - EdgeAddMember = engine.NewEdge("AddMember") - EdgeAddMemberGroupAttr = engine.NewEdge("AddMemberGroupAttr") - EdgeAddSelfMember = engine.NewEdge("AddSelfMember") - EdgeReadMSAPassword = engine.NewEdge("ReadMSAPassword") - EdgeHasMSA = engine.NewEdge("HasMSA") - EdgeWriteKeyCredentialLink = engine.NewEdge("WriteKeyCredentialLink") + EdgeWriteAllowedToAct = engine.NewEdge("WriteAllowedToAct") + EdgeAddMember = engine.NewEdge("AddMember") + EdgeAddMemberGroupAttr = engine.NewEdge("AddMemberGroupAttr") + EdgeAddSelfMember = engine.NewEdge("AddSelfMember") + EdgeReadMSAPassword = engine.NewEdge("ReadMSAPassword") + EdgeHasMSA = engine.NewEdge("HasMSA") + EdgeWriteUserAccountControl = engine.NewEdge("WriteUserAccountControl").Describe("Allows attacker to set ENABLE and set DONT_REQ_PREAUTH and then to AS_REP Kerberoasting").RegisterProbabilityCalculator(func(source, target *engine.Object) engine.Probability { + /*if uac, ok := target.AttrInt(activedirectory.UserAccountControl); ok && uac&0x0002 != 0 { //UAC_ACCOUNTDISABLE + // Account is disabled + return 0 + }*/ + return 50 + }) + + EdgeWriteKeyCredentialLink = engine.NewEdge("WriteKeyCredentialLink").RegisterProbabilityCalculator(func(source, target *engine.Object) engine.Probability { + if uac, ok := target.AttrInt(UserAccountControl); ok && uac&0x0002 /*UAC_ACCOUNTDISABLE*/ != 0 { + // Account is disabled + var canenable bool + source.Edges(engine.Out).Range(func(key *engine.Object, value engine.EdgeBitmap) bool { + if key == target { + if value.IsSet(EdgeWriteUserAccountControl) { + canenable = true + return false + } + } + return true + }) + if !canenable { + return 0 + } + } + return 100 + }) EdgeWriteAttributeSecurityGUID = engine.NewEdge("WriteAttrSecurityGUID").RegisterProbabilityCalculator(func(source, target *engine.Object) engine.Probability { return 5 }) // Only if you patch the DC, so this will actually never work EdgeSIDHistoryEquality = engine.NewEdge("SIDHistoryEquality") EdgeAllExtendedRights = engine.NewEdge("AllExtendedRights") EdgeDSReplicationSyncronize = engine.NewEdge("DSReplSync") - EdgeDSReplicationGetChanges = engine.NewEdge("DSReplGetChngs") - EdgeDSReplicationGetChangesAll = engine.NewEdge("DSReplGetChngsAll") - EdgeDSReplicationGetChangesInFilteredSet = engine.NewEdge("DSReplGetChngsInFiltSet") + EdgeDSReplicationGetChanges = engine.NewEdge("DSReplGetChngs").SetDefault(false, false, false) + EdgeDSReplicationGetChangesAll = engine.NewEdge("DSReplGetChngsAll").SetDefault(false, false, false) + EdgeDSReplicationGetChangesInFilteredSet = engine.NewEdge("DSReplGetChngsInFiltSet").SetDefault(false, false, false) EdgeDCsync = engine.NewEdge("DCsync") EdgeReadLAPSPassword = engine.NewEdge("ReadLAPSPassword") EdgeMemberOfGroup = engine.NewEdge("MemberOfGroup")