Skip to content

Commit

Permalink
Added LAPS v2 detection, predefined query for RODCs, tagged RODCs, va…
Browse files Browse the repository at this point in the history
…rious query fixes
  • Loading branch information
lkarlslund committed Oct 22, 2024
1 parent 6d9432a commit cc158b4
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 120 deletions.
81 changes: 1 addition & 80 deletions modules/analyze/analyzeobjects.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package analyze

import (
"fmt"
"sort"
"strconv"

Expand Down Expand Up @@ -89,38 +88,6 @@ func ParseQueryFromPOST(ctx *gin.Context, objects *engine.Objects) (*AnalyzeOpti
// aoo.NodeLimit = 1000
// }

aoo.FilterFirst, err = query.ParseLDAPQueryStrict(qd.QueryFirst, objects)
if err != nil {
return nil, fmt.Errorf("Error parsing start query: %v", err)
}

if qd.QueryMiddle != "" {
aoo.FilterMiddle, err = query.ParseLDAPQueryStrict(qd.QueryMiddle, objects)
if err != nil {
return nil, fmt.Errorf("Error parsing middle query: %v", err)
}
}

if qd.QueryLast != "" {
aoo.FilterLast, err = query.ParseLDAPQueryStrict(qd.QueryLast, objects)
if err != nil {
return nil, fmt.Errorf("Error parsing end query: %v", err)
}
}

// Parse edges into edge bitmaps
aoo.EdgesFirst, err = engine.EdgeBitmapFromStringSlice(qd.EdgesFirst)
if err != nil {
return nil, err
}
aoo.EdgesMiddle, err = engine.EdgeBitmapFromStringSlice(qd.EdgesMiddle)
if err != nil {
return nil, err
}
aoo.EdgesLast, err = engine.EdgeBitmapFromStringSlice(qd.EdgesLast)
if err != nil {
return nil, err
}
// Default to all edges if none are specified
if aoo.EdgesFirst.Count() == 0 && aoo.EdgesMiddle.Count() == 0 && aoo.EdgesLast.Count() == 0 {
// Spread the choices to FME
Expand Down Expand Up @@ -167,52 +134,6 @@ type AnalysisResults struct {
}

func Analyze(opts AnalyzeOptions, objects *engine.Objects) AnalysisResults {
if opts.EdgesFirst.Count() == 0 {
for _, edge := range engine.Edges() {
if edge.DefaultF() {
opts.EdgesFirst = opts.EdgesFirst.Set(edge)
}
}
}
if opts.EdgesMiddle.Count() == 0 {
for _, edge := range engine.Edges() {
if edge.DefaultM() {
opts.EdgesMiddle = opts.EdgesMiddle.Set(edge)
}
}
}
if opts.EdgesLast.Count() == 0 {
for _, edge := range engine.Edges() {
if edge.DefaultL() {
opts.EdgesLast = opts.EdgesLast.Set(edge)
}
}
}

if len(opts.ObjectTypesFirst) == 0 {
opts.ObjectTypesFirst = make(map[engine.ObjectType]struct{})
for i, ot := range engine.ObjectTypes() {
if ot.DefaultEnabledF {
opts.ObjectTypesFirst[engine.ObjectType(i)] = struct{}{}
}
}
}
if len(opts.ObjectTypesMiddle) == 0 {
opts.ObjectTypesMiddle = make(map[engine.ObjectType]struct{})
for i, ot := range engine.ObjectTypes() {
if ot.DefaultEnabledM {
opts.ObjectTypesMiddle[engine.ObjectType(i)] = struct{}{}
}
}
}
if len(opts.ObjectTypesLast) == 0 {
opts.ObjectTypesLast = make(map[engine.ObjectType]struct{})
for i, ot := range engine.ObjectTypes() {
if ot.DefaultEnabledL {
opts.ObjectTypesLast[engine.ObjectType(i)] = struct{}{}
}
}
}

pg := graph.NewGraph[*engine.Object, engine.EdgeBitmap]()
extrainfo := make(map[*engine.Object]*GraphNode)
Expand Down Expand Up @@ -249,7 +170,7 @@ func Analyze(opts AnalyzeOptions, objects *engine.Objects) AnalysisResults {
}
}

ui.Debug().Msgf("Processing round %v with %v total objects and %v connections", currentRound, pg.Order(), pg.Size())
ui.Debug().Msgf("Starting round %v with %v total objects and %v connections", currentRound, pg.Order(), pg.Size())

nodesatstartofround := pg.Order()

Expand Down
76 changes: 75 additions & 1 deletion modules/analyze/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ func DefaultQueryDefinition() QueryDefinition {
QueryFirst: "(&(objectClass=group)(|(name=Domain Admins)(name=Enterprise Admins)))",
MaxDepth: -1,
MaxOutgoingConnections: -1,
PruneIslands: true,
}
}

Expand Down Expand Up @@ -72,6 +71,76 @@ func (qd QueryDefinition) AnalysisOptions(ao *engine.Objects) (AnalyzeOptions, e
aoo.FilterLast = filter
}

// ObjectTypes

aoo.ObjectTypesFirst = make(map[engine.ObjectType]struct{})
if len(qd.ObjectTypesFirst) == 0 {
for i, ot := range engine.ObjectTypes() {
if ot.DefaultEnabledF {
aoo.ObjectTypesFirst[engine.ObjectType(i)] = struct{}{}
}
}
} else {
for _, otname := range qd.ObjectTypesFirst {
ot, found := engine.ObjectTypeLookup(otname)
if found {
aoo.ObjectTypesFirst[ot] = struct{}{}
}
}
}

aoo.ObjectTypesMiddle = make(map[engine.ObjectType]struct{})
if len(qd.ObjectTypesMiddle) == 0 {
for i, ot := range engine.ObjectTypes() {
if ot.DefaultEnabledM {
aoo.ObjectTypesMiddle[engine.ObjectType(i)] = struct{}{}
}
}
} else {
for _, otname := range qd.ObjectTypesMiddle {
ot, found := engine.ObjectTypeLookup(otname)
if found {
aoo.ObjectTypesMiddle[ot] = struct{}{}
}
}
}

aoo.ObjectTypesLast = make(map[engine.ObjectType]struct{})
if len(qd.ObjectTypesLast) == 0 {
for i, ot := range engine.ObjectTypes() {
if ot.DefaultEnabledL {
aoo.ObjectTypesLast[engine.ObjectType(i)] = struct{}{}
}
}
} else {
for _, otname := range qd.ObjectTypesLast {
ot, found := engine.ObjectTypeLookup(otname)
if found {
aoo.ObjectTypesLast[ot] = struct{}{}
}
}
}

// Edgetypes

if len(qd.EdgesFirst) > 0 {
aoo.EdgesFirst, err = engine.EdgeBitmapFromStringSlice(qd.EdgesFirst)
if err != nil {
return aoo, err
}
}
if len(qd.EdgesMiddle) > 0 {
aoo.EdgesMiddle, err = engine.EdgeBitmapFromStringSlice(qd.EdgesMiddle)
if err != nil {
return aoo, err
}
}
if len(qd.EdgesLast) > 0 {
aoo.EdgesLast, err = engine.EdgeBitmapFromStringSlice(qd.EdgesLast)
if err != nil {
return aoo, err
}
}
aoo.MaxDepth = qd.MaxDepth
aoo.MaxOutgoingConnections = qd.MaxOutgoingConnections
aoo.Direction = qd.Direction
Expand Down Expand Up @@ -201,6 +270,11 @@ var (
QueryFirst: "(&(type=Machine)(out=MachineAccount,(&(type=Computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))))",
Direction: engine.In,
},
{
Name: "Who can reach Read-Only Domain Controllers? (RODC)",
QueryFirst: "(&(type=Machine)(out=MachineAccount,(&(type=Computer)(primaryGroupId=521))))",
Direction: engine.In,
},
{
Name: "Who can reach computers with unconstrained delegation (non DCs)?",
QueryFirst: "(&(type=Computer)(userAccountControl:1.2.840.113556.1.4.803:=524288)(!userAccountControl:1.2.840.113556.1.4.803:=8192))",
Expand Down
3 changes: 3 additions & 0 deletions modules/engine/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,14 @@ func init() {
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) {
Expand Down Expand Up @@ -247,6 +249,7 @@ func (a Attribute) Merge() Attribute {
return a
}

// AddMergeApprover adds a new function that can object to an object merge, or forever hold its silence
func AddMergeApprover(name string, mf mergefunc) {
attributemutex.Lock()
mergeapprovers = append(mergeapprovers, mergeapproverinfo{
Expand Down
2 changes: 1 addition & 1 deletion modules/engine/edge.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func EdgeBitmapFromStringSlice(edgenames []string) (eb EdgeBitmap, err error) {
err = ErrEdgeNotFound
return
}
eb.Set(edge)
eb = eb.Set(edge)
}
return
}
Expand Down
114 changes: 99 additions & 15 deletions modules/integrations/activedirectory/analyze/analyze-ad.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ var (
MetaPasswordAge = engine.NewAttribute("passwordAge")
MetaLastLoginAge = engine.NewAttribute("lastLoginAge")

msLAPSEncryptedPasswordAttributesGUID, _ = uuid.FromString("{f3531ec6-6330-4f8e-8d39-7a671fbac605}")

EdgeMachineAccount = engine.NewEdge("MachineAccount").RegisterProbabilityCalculator(activedirectory.FixedProbability(-1)).Describe("Indicates this is the domain joined computer account belonging to the machine")
)

Expand All @@ -102,23 +104,18 @@ func init() {
LoaderID.AddProcessor(func(ao *engine.Objects) {
// Find LAPS or return
var lapsGUID uuid.UUID
if lapsobjects, found := ao.FindMulti(engine.Name, engine.AttributeValueString("ms-Mcs-AdmPwd")); found {
lapsobjects.Iterate(func(lapsobject *engine.Object) bool {
if lapsobject.HasAttrValue(engine.ObjectClass, engine.AttributeValueString("attributeSchema")) {
if objectGUID, ok := lapsobject.OneAttrRaw(activedirectory.SchemaIDGUID).(uuid.UUID); ok {
ui.Debug().Msg("Detected LAPS schema extension GUID")
lapsGUID = objectGUID
return false // break
} else {
ui.Error().Msgf("Could not read LAPS schema extension GUID from %v", lapsobject.DN())
}
}
return true
})
if lapsobject, found := ao.FindTwo(engine.Name, engine.AttributeValueString("ms-Mcs-AdmPwd"),
engine.ObjectClass, engine.AttributeValueString("attributeSchema")); found {
if objectGUID, ok := lapsobject.OneAttrRaw(activedirectory.SchemaIDGUID).(uuid.UUID); ok {
ui.Debug().Msg("Detected LAPS schema extension GUID")
lapsGUID = objectGUID
} else {
ui.Error().Msgf("Could not read LAPS schema extension GUID from %v", lapsobject.DN())
}
}

if lapsGUID.IsNil() {
ui.Debug().Msg("Microsoft LAPS not detected, skipping tests for this")
ui.Debug().Msg("Microsoft LAPS V1 not detected, skipping tests for this")
return
}

Expand Down Expand Up @@ -157,7 +154,80 @@ func init() {
}
return true
})
}, "Reading local admin passwords via LAPS", engine.BeforeMergeFinal)
}, "Reading local admin passwords via LAPS v1", engine.BeforeMergeFinal)

LoaderID.AddProcessor(func(ao *engine.Objects) {
// Find LAPS or return
var lapsV2PasswordGUID uuid.UUID
var lapsV2EncryptedPasswordGUID uuid.UUID

if lapsobject, found := ao.FindTwo(engine.Name, engine.AttributeValueString("ms-LAPS-Password"),
engine.ObjectClass, engine.AttributeValueString("attributeSchema")); found {
if objectGUID, ok := lapsobject.OneAttrRaw(activedirectory.SchemaIDGUID).(uuid.UUID); ok {
ui.Debug().Msg("Detected LAPS schema extension GUID")
lapsV2PasswordGUID = objectGUID
} else {
ui.Error().Msgf("Could not read LAPS schema extension GUID from %v", lapsobject.DN())
}
}
if lapsobject, found := ao.FindTwo(engine.Name, engine.AttributeValueString("ms-LAPS-EncryptedPassword"),
engine.ObjectClass, engine.AttributeValueString("attributeSchema")); found {
if objectGUID, ok := lapsobject.OneAttrRaw(activedirectory.SchemaIDGUID).(uuid.UUID); ok {
ui.Debug().Msg("Detected LAPS schema extension GUID")
lapsV2EncryptedPasswordGUID = objectGUID
} else {
ui.Error().Msgf("Could not read LAPS schema extension GUID from %v", lapsobject.DN())
}
}

if lapsV2PasswordGUID.IsNil() {
ui.Debug().Msg("Microsoft LAPS V2 not detected, skipping tests for this")
return
}

ao.Iterate(func(o *engine.Object) bool {
// Only for computers
if o.Type() != engine.ObjectTypeComputer {
return true
}

// ... that has LAPS installed
if !o.HasAttr(activedirectory.MSLAPSPasswordExpirationTime) {
return true
}

// Analyze ACL
sd, err := o.SecurityDescriptor()
if err != nil {
return true
}

// Link to the machine object
machinesid := o.SID()
if machinesid.IsBlank() {
ui.Fatal().Msgf("Computer account %v has no objectSID", o.DN())
}
machine, found := ao.Find(DomainJoinedSID, engine.AttributeValueSID(machinesid))
if !found {
ui.Error().Msgf("Could not locate machine for domain SID %v", machinesid)
return true
}

for index, acl := range sd.DACL.Entries {
if sd.DACL.IsObjectClassAccessAllowed(index, o, engine.RIGHT_DS_CONTROL_ACCESS, lapsV2PasswordGUID, ao) {
ao.FindOrAddAdjacentSID(acl.SID, o).EdgeTo(machine, activedirectory.EdgeReadLAPSPassword)
}
if sd.DACL.IsObjectClassAccessAllowed(index, o, engine.RIGHT_DS_CONTROL_ACCESS, lapsV2EncryptedPasswordGUID, ao) {
ao.FindOrAddAdjacentSID(acl.SID, o).EdgeTo(machine, activedirectory.EdgeReadLAPSPassword) // FIXME
}

if sd.DACL.IsObjectClassAccessAllowed(index, o, engine.RIGHT_DS_CONTROL_ACCESS, msLAPSEncryptedPasswordAttributesGUID, ao) {
ao.FindOrAddAdjacentSID(acl.SID, o).EdgeTo(machine, activedirectory.EdgeReadLAPSPassword) // FIXME
}
}
return true
})
}, "Reading local admin passwords via LAPS v2", engine.BeforeMergeFinal)

LoaderID.AddProcessor(func(ao *engine.Objects) {
ao.Iterate(func(o *engine.Object) bool {
Expand Down Expand Up @@ -1320,6 +1390,20 @@ func init() {
}
}
}

if object.HasAttrValue(activedirectory.PrimaryGroupID, engine.AttributeValueInt(521)) {
// Read Only Domain Controller
machine, found := ao.FindTwo(engine.Type, engine.AttributeValueString("Machine"),
DomainJoinedSID, engine.AttributeValueSID(object.SID()))
if !found {
ui.Warn().Msgf("Can not find machine object for RODC %v", object.DN())
} else {
machine.Tag("role-readonly-domaincontroller")
}

// Figure out what hashes this machine has cached - FIXME!

}
}

if object.Type() == engine.ObjectTypeTrust {
Expand Down
Loading

0 comments on commit cc158b4

Please sign in to comment.