From 687e3181fdd690ee4316f58e0eb7433e6813593d Mon Sep 17 00:00:00 2001 From: Lars Karlslund Date: Thu, 24 Feb 2022 12:47:59 +0100 Subject: [PATCH] Added cPassword analysis to GPO --- modules/engine/object.go | 87 ++++++++++++++--- .../activedirectory/analyze/gpoimport.go | 97 +++++++++++++++++-- .../activedirectory/collect/cli.go | 6 ++ modules/integrations/activedirectory/gpo.go | 8 +- 4 files changed, 172 insertions(+), 26 deletions(-) diff --git a/modules/engine/object.go b/modules/engine/object.go index b1ca527..4dccdc5 100644 --- a/modules/engine/object.go +++ b/modules/engine/object.go @@ -1,6 +1,7 @@ package engine import ( + "encoding/xml" "errors" "fmt" "strconv" @@ -39,18 +40,21 @@ func setThreadsafe(enable bool) { var UnknownGUID = uuid.UUID{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} type Object struct { - values AttributeValueMap - PwnableBy PwnConnections - CanPwn PwnConnections - parent *Object - sdcache *SecurityDescriptor - sid windowssecurity.SID - children []*Object - members []*Object + values AttributeValueMap + PwnableBy PwnConnections + CanPwn PwnConnections + parent *Object + sdcache *SecurityDescriptor + sid windowssecurity.SID + children []*Object + members []*Object + membersrecursive []*Object + // objectclassguids []uuid.UUID - memberof []*Object - id uint32 - guid uuid.UUID + memberof []*Object + memberofrecursive []*Object + id uint32 + guid uuid.UUID // objectcategoryguid uuid.UUID guidcached bool sidcached bool @@ -223,6 +227,8 @@ func (o *Object) Absorb(source *Object) { } target.objecttype = 0 // Recalculate this + target.memberofrecursive = nil + target.membersrecursive = nil } func (o *Object) AttributeValueMap() AttributeValueMap { @@ -237,8 +243,48 @@ func (o *Object) AttributeValueMap() AttributeValueMap { return val } +type StringMap map[string][]string + +func (s StringMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + + tokens := []xml.Token{start} + + for key, values := range s { + t := xml.StartElement{Name: xml.Name{"", key}} + for _, value := range values { + tokens = append(tokens, t, xml.CharData(value), xml.EndElement{t.Name}) + } + } + + tokens = append(tokens, xml.EndElement{start.Name}) + + for _, t := range tokens { + err := e.EncodeToken(t) + if err != nil { + return err + } + } + + // flush to ensure tokens are written + return e.Flush() +} + +func (o *Object) NameStringMap() StringMap { + o.lock() + defer o.unlock() + result := make(StringMap) + for attr, values := range o.values { + result[attr.String()] = values.StringSlice() + } + return result +} + func (o *Object) MarshalJSON() ([]byte, error) { - return jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(o.AttributeValueMap()) + return jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(o.NameStringMap()) +} + +func (o *Object) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + return o.NameStringMap().MarshalXML(e, start) } func (o *Object) IDString() string { @@ -504,10 +550,14 @@ func (o *Object) recursemembers(members *map[*Object]struct{}) { func (o *Object) MemberOf(recursive bool) []*Object { o.lock() defer o.unlock() - if !recursive { + if !recursive || len(o.memberof) == 0 { return o.memberof } + if o.memberofrecursive != nil { + return o.memberofrecursive + } + memberof := make(map[*Object]struct{}) o.recursememberof(&memberof) @@ -517,18 +567,25 @@ func (o *Object) MemberOf(recursive bool) []*Object { memberofarray[i] = member i++ } + o.memberofrecursive = memberofarray return memberofarray } -func (o *Object) recursememberof(memberof *map[*Object]struct{}) { +// Recursive memberof, returns true if loop is detected +func (o *Object) recursememberof(memberof *map[*Object]struct{}) bool { + var loop bool for _, directmemberof := range o.memberof { if _, found := (*memberof)[directmemberof]; found { // endless loop, not today thanks + loop = true continue } (*memberof)[directmemberof] = struct{}{} - directmemberof.recursemembers(memberof) + if directmemberof.recursememberof(memberof) { + loop = true + } } + return loop } // Wrapper for Set - easier to call diff --git a/modules/integrations/activedirectory/analyze/gpoimport.go b/modules/integrations/activedirectory/analyze/gpoimport.go index aff71c3..4813878 100644 --- a/modules/integrations/activedirectory/analyze/gpoimport.go +++ b/modules/integrations/activedirectory/analyze/gpoimport.go @@ -18,15 +18,21 @@ import ( var ( gPCFileSysPath = engine.NewAttribute("gPCFileSysPath").Merge() - AbsolutePath = engine.NewAttribute("AbsolutePath") - RelativePath = engine.NewAttribute("RelativePath") - PwnOwns = engine.NewPwn("Owns") - PwnFSPartOfGPO = engine.NewPwn("FSPartOfGPO") - PwnFileCreate = engine.NewPwn("FileCreate") - PwnDirCreate = engine.NewPwn("DirCreate") - PwnFileWrite = engine.NewPwn("FileWrite") - PwnTakeOwnership = engine.NewPwn("FileTakeOwnership") - PwnModifyDACL = engine.NewPwn("FileModifyDACL") + AbsolutePath = engine.NewAttribute("absolutePath") + RelativePath = engine.NewAttribute("relativePath") + BinarySize = engine.NewAttribute("binarySize") + ExposedPassword = engine.NewAttribute("exposedPassword") + + PwnExposesPassword = engine.NewPwn("ExposesPassword") + PwnContainsSensitiveData = engine.NewPwn("ContainsSensitiveData") + PwnReadSensitiveData = engine.NewPwn("ReadSensitiveData") + PwnOwns = engine.NewPwn("Owns") + PwnFSPartOfGPO = engine.NewPwn("FSPartOfGPO") + PwnFileCreate = engine.NewPwn("FileCreate") + PwnDirCreate = engine.NewPwn("DirCreate") + PwnFileWrite = engine.NewPwn("FileWrite") + PwnTakeOwnership = engine.NewPwn("FileTakeOwnership") + PwnModifyDACL = engine.NewPwn("FileModifyDACL") ) func init() { @@ -38,6 +44,9 @@ func init() { }) } +var cpasswordusername = regexp.MustCompile(`cpassword="(?P[^"]+)[^>]+(runAs|userName)="(?P[^"]+)"`) +var usernamecpassword = regexp.MustCompile(`(runAs|userName)="(?P[^"]+)[^>]+cpassword="(?P[^"]+)"`) + func ImportGPOInfo(ginfo activedirectory.GPOdump, ao *engine.Objects) error { if ginfo.DomainDN != "" { ao.AddDefaultFlex(engine.UniqueSource, ginfo.DomainDN) @@ -61,10 +70,13 @@ func ImportGPOInfo(ginfo activedirectory.GPOdump, ao *engine.Objects) error { } itemobject := ao.AddNew( + engine.IgnoreBlanks, AbsolutePath, engine.AttributeValueString(absolutepath), RelativePath, engine.AttributeValueString(relativepath), engine.DisplayName, engine.AttributeValueString(relativepath), engine.ObjectCategorySimple, engine.AttributeValueString(objecttype), + BinarySize, engine.AttributeValueInt(item.Size), + activedirectory.WhenChanged, engine.AttributeValueTime(item.Timestamp), ) if relativepath == "/" { @@ -114,6 +126,73 @@ func ImportGPOInfo(ginfo activedirectory.GPOdump, ao *engine.Objects) error { } } + var exposed []struct{ Username, Password string } + + for _, line := range strings.Split(string(item.Contents), "\n") { + var unhandledpass bool + + // FIXME: Handle other formats, adding something to catch this here + if strings.Contains(line, "cpassword=") && !strings.Contains(line, "cpassword=\"\"") { + log.Debug().Msgf("Found cpassword in %s", item.RelativePath) + log.Debug().Msgf("GPO Dump\n%s", item.Contents) + unhandledpass = true + } + for _, match := range cpasswordusername.FindAllStringSubmatch(line, -1) { + log.Debug().Msgf("Found password in %s", item.RelativePath) + log.Debug().Msgf("Password: %v", match) + log.Debug().Msgf("GPO Dump\n%s", item.Contents) + exposed = append(exposed, struct{ Username, Password string }{match[cpasswordusername.SubexpIndex("username")], match[cpasswordusername.SubexpIndex("password")]}) + unhandledpass = false + } + for _, match := range usernamecpassword.FindAllStringSubmatch(line, -1) { + log.Debug().Msgf("Found username in %s", item.RelativePath) + log.Debug().Msgf("Password: %v", match) + log.Debug().Msgf("GPO Dump\n%s", item.Contents) + exposed = append(exposed, struct{ Username, Password string }{match[usernamecpassword.SubexpIndex("username")], match[usernamecpassword.SubexpIndex("password")]}) + unhandledpass = false + } + if unhandledpass { + log.Error().Msgf("Unhandled password in %s", item.RelativePath) + log.Error().Msgf("GPO Dump\n%s", item.Contents) + log.Panic().Msg("Please submit bugreport on Github with redacted account name and redacted password") + } + } + for _, e := range exposed { + // New object to contain the sensitive data + expobj := ao.AddNew( + engine.ObjectCategorySimple, "ExposedPassword", + engine.DisplayName, "Exposed password for "+e.Username, + ExposedPassword, e.Password, + ) + + // The account targeted + target, _ := ao.FindOrAdd( + engine.DownLevelLogonName, engine.AttributeValueString(e.Username), + ) + + // GPO exposes this object + itemobject.Pwns(expobj, PwnContainsSensitiveData) + // Exposed password leaks this object + expobj.Pwns(target, PwnExposesPassword) + + // Everyone that can read the file can then read the password + if item.DACL != nil { + dacl, err := engine.ParseACL(item.DACL) + if err != nil { + return err + } + for _, entry := range dacl.Entries { + entrysidobject, _ := ao.FindOrAdd(activedirectory.ObjectSid, engine.AttributeValueSID(entry.SID)) + + if entry.Type == engine.ACETYPE_ACCESS_ALLOWED && entry.SID.Component(2) == 21 { + if entry.Mask&engine.FILE_READ_DATA != 0 { + entrysidobject.Pwns(expobj, PwnReadSensitiveData) + } + } + } + } + + } switch relativepath { case "/machine/preferences/groups/groups.xml", "/machine/microsoft/windows nt/secedit/gpttmpl.inf": var pairs []SIDpair diff --git a/modules/integrations/activedirectory/collect/cli.go b/modules/integrations/activedirectory/collect/cli.go index b33fddc..188d508 100644 --- a/modules/integrations/activedirectory/collect/cli.go +++ b/modules/integrations/activedirectory/collect/cli.go @@ -393,6 +393,12 @@ func Execute(cmd *cobra.Command, args []string) error { var fileinfo activedirectory.GPOfileinfo fileinfo.IsDir = d.IsDir() + if !fileinfo.IsDir { + if info, err := d.Info(); err == nil { + fileinfo.Timestamp = info.ModTime() + fileinfo.Size = info.Size() + } + } fileinfo.RelativePath = curpath[offset:] if gppath == originalpath { diff --git a/modules/integrations/activedirectory/gpo.go b/modules/integrations/activedirectory/gpo.go index cce3597..52f6406 100644 --- a/modules/integrations/activedirectory/gpo.go +++ b/modules/integrations/activedirectory/gpo.go @@ -1,6 +1,8 @@ package activedirectory import ( + "time" + "github.com/gofrs/uuid" "github.com/lkarlslund/adalanche/modules/basedata" "github.com/lkarlslund/adalanche/modules/windowssecurity" @@ -22,8 +24,10 @@ type GPOfileinfo struct { RelativePath string `json:",omitempty"` IsDir bool `json:",omitempty"` - OwnerSID windowssecurity.SID `json:",omitempty"` - DACL []byte `json:",omitempty"` + Size int64 `json:",omitempty"` + Timestamp time.Time `json:",omitempty"` + OwnerSID windowssecurity.SID `json:",omitempty"` + DACL []byte `json:",omitempty"` Contents []byte `json:",omitempty"` }