● Notice //GitHub (@shehzade) is temporarily suspended pending an automated false-positive review. All tools are hosted on this Code Mirror and available on request (contact); GitHub links will return once reinstated.
PUBLIC MIRRORGo v1.0.0

Offline Active Directory database (ntds.dit) parser and BloodHound graph reconstructor in pure Go. Extracts directory objects, group memberships, domain trusts, and binary DACLs from dead-disk ESE database pages and registry hives.

$ git clone https://shehzade.io/repos/ntds-to-bloodhound.git Copy
Source ZIP (246.9 KB) Prebuilt Binaries
Code 21 Commits 1 Releases 5
ntds-to-bloodhound / pkg / acl / engine.go
← Back to directory
134 lines · 2.5 KB
Raw
1package acl
2
3import (
4 "encoding/binary"
5 "encoding/hex"
6
7 "github.com/Velocidex/ordereddict"
8 "github.com/shehzade/ntds-to-bloodhound/pkg/ese"
9)
10
11type cacheKey struct {
12 sdID uint64
13 objType string
14 hasLAPS bool
15}
16
17type cachedSD struct {
18 aces []Ace
19 isProtected bool
20}
21
22type SecurityEngine struct {
23 RawSDs map[uint64][]byte
24 AclCache map[cacheKey]cachedSD
25 LapsGUIDs map[string]bool
26}
27
28func NewSecurityEngine(db *ese.DB) (*SecurityEngine, error) {
29 engine := &SecurityEngine{
30 RawSDs: make(map[uint64][]byte),
31 AclCache: make(map[cacheKey]cachedSD),
32 LapsGUIDs: make(map[string]bool),
33 }
34
35 aliases := make(map[uint64]uint64)
36
37 err := db.StreamTable("sd_table", func(row *ordereddict.Dict) error {
38 idVal, ok := row.Get("sd_id")
39 if !ok || idVal == nil {
40 return nil
41 }
42
43 var id uint64
44 switch v := idVal.(type) {
45 case uint64:
46 id = v
47 case int64:
48 id = uint64(v)
49 case uint32:
50 id = uint64(v)
51 case int32:
52 id = uint64(v)
53 case int:
54 id = uint64(v)
55 }
56
57 val, _ := row.Get("sd_value")
58 hexStr, ok := val.(string)
59 if !ok || len(hexStr) == 0 {
60 return nil
61 }
62
63 b, err := hex.DecodeString(hexStr)
64 if err != nil {
65 return nil
66 }
67
68 if len(b) >= 20 {
69 engine.RawSDs[id] = b
70 } else if len(b) == 4 {
71 targetID := uint64(binary.LittleEndian.Uint32(b))
72 aliases[id] = targetID
73 }
74
75 return nil
76 })
77 if err != nil {
78 return nil, err
79 }
80
81 for srcID := range aliases {
82 curr := srcID
83 visited := make(map[uint64]bool)
84 for {
85 if visited[curr] {
86 break
87 }
88 visited[curr] = true
89 if target, ok := aliases[curr]; ok {
90 curr = target
91 } else {
92 break
93 }
94 }
95 if rawSD, exists := engine.RawSDs[curr]; exists {
96 engine.RawSDs[srcID] = rawSD
97 }
98 }
99
100 return engine, nil
101}
102
103func (e *SecurityEngine) Count() int {
104 return len(e.RawSDs)
105}
106
107func (e *SecurityEngine) GetAces(sdID uint64) []Ace {
108 aces, _ := e.GetAcesForObject(sdID, "", false)
109 return aces
110}
111
112func (e *SecurityEngine) GetAcesForObject(sdID uint64, objectType string, hasLAPS bool) ([]Ace, bool) {
113 key := cacheKey{sdID: sdID, objType: objectType, hasLAPS: hasLAPS}
114 if cached, exists := e.AclCache[key]; exists {
115 out := make([]Ace, len(cached.aces))
116 copy(out, cached.aces)
117 return out, cached.isProtected
118 }
119
120 rawSD, exists := e.RawSDs[sdID]
121 if !exists {
122 return nil, false
123 }
124
125 aces, isProtected, err := ParseSecurityDescriptorForType(rawSD, objectType, hasLAPS, e.LapsGUIDs)
126 if err != nil {
127 return nil, false
128 }
129
130 e.AclCache[key] = cachedSD{aces: aces, isProtected: isProtected}
131 out := make([]Ace, len(aces))
132 copy(out, aces)
133 return out, isProtected
134}