Skip to content

Commit

Permalink
Reworked indexes, merging and deduplication of dual-loaded distinguis…
Browse files Browse the repository at this point in the history
…hedName objects - also CNF and DEL objects are not loaded default now
  • Loading branch information
lkarlslund committed May 4, 2022
1 parent 3dd0f7b commit f1fa2f6
Show file tree
Hide file tree
Showing 14 changed files with 738 additions and 451 deletions.
7 changes: 7 additions & 0 deletions modules/analyze/html/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,13 @@ var cytostyle = [{
"background-color": "lightgreen"
}
},
{
selector: 'node[type="GroupManagedServiceAccount"]',
style: {
"background-image": "icons/manage_accounts_black_24dp.svg",
"background-color": "lightgreen"
}
},
{
selector: 'node[type="ForeignSecurityPrincipal"]',
style: {
Expand Down
2 changes: 1 addition & 1 deletion modules/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func Run() error {

if *cpuprofiletimeout > 0 {
go func() {
time.Sleep(time.Second * (time.Duration(*cpuprofiletimeout)))
<-time.After(time.Second * (time.Duration(*cpuprofiletimeout)))
stopprofile <- true
}()
}
Expand Down
7 changes: 5 additions & 2 deletions modules/engine/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ var (
Name = NewAttribute("name").Single()
DisplayName = NewAttribute("displayName").Single()
LDAPDisplayName = NewAttribute("lDAPDisplayName").Single()
Description = NewAttribute("description").Single()
Description = NewAttribute("description")
SAMAccountName = NewAttribute("sAMAccountName").Single()
ObjectSid = NewAttribute("objectSid").Single() // Strange yes, but in the final results there are multiple objects with the same SID
ObjectGUID = NewAttribute("objectGUID").Single().Unique()
Expand All @@ -66,7 +66,9 @@ var (
RightsGUID = NewAttribute("rightsGUID")
AttributeSecurityGUID = NewAttribute("attributeSecurityGUID")

WhenCreated = NewAttribute("whenCreated").Single()
WhenChanged = NewAttribute("whenChanged").Type(AttributeTypeTime) // Not replicated, so we're not marking it as "single"

WhenCreated = NewAttribute("whenCreated").Single().Type(AttributeTypeTime)

ObjectClassGUIDs = NewAttribute("objectClassGUID") // Used for caching the GUIDs, should belong in AD analyzer, but it's used in the SecurityDescritor mapping, so we're cheating a bit
ObjectCategoryGUID = NewAttribute("objectCategoryGUID") // Used for caching the GUIDs
Expand All @@ -80,6 +82,7 @@ var (
DownLevelLogonName = NewAttribute("downLevelLogonName").Merge()
UserPrincipalName = NewAttribute("userPrincipalName").Merge()
NetbiosDomain = NewAttribute("netbiosDomain") // Used to merge users with - if we only have a DOMAIN\USER type of info
DomainPart = NewAttribute("domainPart")

MetaProtectedUser = NewAttribute("_protecteduser")
MetaUnconstrainedDelegation = NewAttribute("_unconstraineddelegation")
Expand Down
5 changes: 5 additions & 0 deletions modules/engine/attributevalue.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ func CompareAttributeValues(a, b AttributeValue) bool {
if btype {
return na == nb
}
case *Object:
nb, btype := braw.(*Object)
if btype {
return na == nb // Exact same object pointed to in memory
}
default:
// Fallback
return a.String() == b.String()
Expand Down
43 changes: 43 additions & 0 deletions modules/engine/flexlock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package engine

import "sync"

type FlexMutex struct {
m sync.RWMutex
enabled uint64
}

func (fm *FlexMutex) Lock() {
if fm.enabled != 0 {
fm.m.Lock()
}
}

func (fm *FlexMutex) Unlock() {
if fm.enabled != 0 {
fm.m.Unlock()
}
}

func (fm *FlexMutex) RLock() {
if fm.enabled != 0 {
fm.m.RLock()
}
}

func (fm *FlexMutex) RUnlock() {
if fm.enabled != 0 {
fm.m.RUnlock()
}
}

func (fm *FlexMutex) Enable() {
fm.enabled++
}

func (fm *FlexMutex) Disable() {
if fm.enabled == 0 {
panic("FlexMutex is already disabled")
}
fm.enabled--
}
87 changes: 55 additions & 32 deletions modules/engine/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/rs/zerolog/log"
)

var adjustthreadsafe sync.Mutex
var threadsafeobject int

const threadbuckets = 1024
Expand All @@ -31,6 +32,7 @@ func init() {
}

func setThreadsafe(enable bool) {
adjustthreadsafe.Lock()
if enable {
threadsafeobject++
} else {
Expand All @@ -39,6 +41,7 @@ func setThreadsafe(enable bool) {
if threadsafeobject < 0 {
panic("threadsafeobject is negative")
}
adjustthreadsafe.Unlock()
}

var UnknownGUID = uuid.UUID{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
Expand Down Expand Up @@ -121,6 +124,10 @@ func (o *Object) runlock() {
// Absorbs data and Pwn relationships from another object, sucking the soul out of it
// The sources empty shell should be discarded afterwards (i.e. not appear in an Objects collection)
func (o *Object) Absorb(source *Object) {
if o == source {
log.Fatal().Msg("Can't absorb myself")
}

o.lock()
if source.lockbucket() != o.lockbucket() {
source.lock()
Expand Down Expand Up @@ -177,18 +184,20 @@ func (o *Object) Absorb(source *Object) {
for pwntarget, methods := range source.CanPwn {
target.CanPwn[pwntarget] = target.CanPwn[pwntarget].Merge(methods)
delete(source.CanPwn, pwntarget)

pwntarget.PwnableBy[target] = pwntarget.PwnableBy[target].Merge(methods)
delete(pwntarget.PwnableBy, source)
}

for pwner, methods := range source.PwnableBy {
target.PwnableBy[pwner] = target.PwnableBy[pwner].Merge(methods)
delete(source.PwnableBy, pwner)

pwner.CanPwn[target] = pwner.CanPwn[target].Merge(methods)
delete(pwner.CanPwn, source)
}

if len(source.members) > 0 {
if len(target.members) > 0 {
members := make(map[*Object]struct{})
for _, member := range target.members {
members[member] = struct{}{}
Expand All @@ -211,7 +220,7 @@ func (o *Object) Absorb(source *Object) {
target.members = source.members
}

if len(source.memberof) > 0 {
if len(target.memberof) > 0 {
memberofgroups := make(map[*Object]struct{})
for _, memberof := range target.memberof {
memberofgroups[memberof] = struct{}{}
Expand All @@ -232,7 +241,13 @@ func (o *Object) Absorb(source *Object) {
}

// Move the securitydescriptor, as we dont have the attribute saved to regenerate it (we throw it away at import after populating the cache)
if target.sdcache == nil && source.sdcache != nil {
if source.sdcache != nil && target.sdcache != nil {
// Both has a cache
if !source.sdcache.Equals(target.sdcache) {
// Different caches, so we need to merge them which is impossible
log.Warn().Msgf("Can not merge security descriptors between %v and %v", source.Label(), target.Label())
}
} else if target.sdcache == nil && source.sdcache != nil {
target.sdcache = source.sdcache
} else {
target.sdcache = nil
Expand Down Expand Up @@ -800,21 +815,30 @@ func (o *Object) set(a Attribute, values AttributeValues) {

if a == DownLevelLogonName {
// There's been so many problems with DLLN that we're going to just check for these
strval := values.StringSlice()[0]
if strval == "," {
log.Warn().Msgf("Setting DownLevelLogonName to ',' is not allowed")
}
if strval == "" {
log.Warn().Msgf("Setting DownLevelLogonName to blank is not allowed")
}
if strings.HasPrefix(strval, "S-") {
log.Warn().Msgf("DownLevelLogonName contains SID: %v", values.StringSlice())
}
if values.Len() != 1 {
log.Warn().Msgf("Found DownLevelLogonName with multiple values: %v", strings.Join(values.StringSlice(), ", "))
}
if strings.HasSuffix(strval, "\\") {
panic("DownLevelLogon ends with \\")
for _, dlln := range values.StringSlice() {
if dlln == "," {
log.Fatal().Msgf("Setting DownLevelLogonName to ',' is not allowed")
}
if dlln == "" {
log.Fatal().Msgf("Setting DownLevelLogonName to blank is not allowed")
}
if strings.HasPrefix(dlln, "S-") {
log.Warn().Msgf("DownLevelLogonName contains SID: %v", values.StringSlice())
}
if strings.HasSuffix(dlln, "\\") {
log.Fatal().Msgf("DownLevelLogonName %v ends with \\", dlln)
}

dotpos := strings.Index(dlln, ".")
if dotpos >= 0 {
backslashpos := strings.Index(dlln, "\\")
if dotpos < backslashpos {
log.Warn().Msgf("DownLevelLogonName contains dot in domain: %v", dlln)
}
}
}
}

Expand Down Expand Up @@ -918,13 +942,24 @@ func (o *Object) String(ao *Objects) string {
return result
}

// Dump the object to simple map type for debugging
func (o *Object) ValueMap() map[string][]string {
result := make(map[string][]string)
for attr, values := range o.values {
result[attr.String()] = values.StringSlice()
}
return result
}

// Return parsed security descriptor
func (o *Object) SecurityDescriptor() (*SecurityDescriptor, error) {
if o.sdcache == nil {
return nil, errors.New("no security desciptor")
}
return o.sdcache, nil
}

// Parse and cache security descriptor
func (o *Object) cacheSecurityDescriptor(rawsd []byte) error {
if len(rawsd) == 0 {
return errors.New("empty nTSecurityDescriptor attribute!?")
Expand All @@ -945,10 +980,12 @@ func (o *Object) cacheSecurityDescriptor(rawsd []byte) error {
o.sdcache = &sd
securityDescriptorCache[cacheindex] = &sd
}

securitydescriptorcachemutex.Unlock()
return err
}

// Return the object's SID
func (o *Object) SID() windowssecurity.SID {
if !o.sidcached {
o.lock()
Expand All @@ -966,6 +1003,7 @@ func (o *Object) SID() windowssecurity.SID {
return sid
}

// Return the object's GUID
func (o *Object) GUID() uuid.UUID {
o.lock()
if !o.guidcached {
Expand All @@ -983,10 +1021,12 @@ func (o *Object) GUID() uuid.UUID {
return guid
}

// Register that this object can pwn another object using the given method
func (o *Object) Pwns(target *Object, method PwnMethod) {
o.PwnsEx(target, method, false)
}

// Enhanched Pwns function that allows us to force the pwn (normally self-pwns are filtered out)
func (o *Object) PwnsEx(target *Object, method PwnMethod, force bool) {
if !force {
if o == target { // SID check solves (some) dual-AD analysis problems
Expand Down Expand Up @@ -1016,23 +1056,6 @@ func (o *Object) PwnsEx(target *Object, method PwnMethod, force bool) {
target.unlock()
}

/*
func (o *Object) Dedup() {
o.DistinguishedName = stringdedup.S(o.DistinguishedName)
for key, values := range o.Attributes {
if key >= MAX_DEDUP {
continue
}
for index, str := range values {
if len(str) < 64 {
values[index] = stringdedup.S(str)
}
}
o.Attributes[key] = values
}
}
*/

func (o *Object) ChildOf(parent *Object) {
o.lock()
if o.parent != nil {
Expand Down
Loading

0 comments on commit f1fa2f6

Please sign in to comment.