● 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 / sd.go
← Back to directory
381 lines · 11.9 KB
Raw
1package acl
2
3import (
4 "encoding/binary"
5 "fmt"
6 "strings"
7
8 "github.com/shehzade/ntds-to-bloodhound/pkg/crypto"
9)
10
11type Ace struct {
12 PrincipalSID string `json:"PrincipalSID"`
13 PrincipalType string `json:"PrincipalType"`
14 RightName string `json:"RightName"`
15 IsInherited bool `json:"IsInherited"`
16}
17
18const (
19 EdgeOwns = "Owns"
20 EdgeGenericAll = "GenericAll"
21 EdgeWriteDacl = "WriteDacl"
22 EdgeWriteOwner = "WriteOwner"
23 EdgeGenericWrite = "GenericWrite"
24 EdgeAllExtendedRights = "AllExtendedRights"
25 EdgeForceChangePassword = "ForceChangePassword"
26 EdgeGetChanges = "GetChanges"
27 EdgeGetChangesAll = "GetChangesAll"
28 EdgeGetChangesInFilteredSet = "GetChangesInFilteredSet"
29 EdgeAddMember = "AddMember"
30 EdgeAddSelf = "AddSelf"
31 EdgeAddAllowedToAct = "AddAllowedToAct"
32 EdgeAddKeyCredentialLink = "AddKeyCredentialLink"
33 EdgeWriteSPN = "WriteSPN"
34 EdgeWriteAccountRestrictions = "WriteAccountRestrictions"
35 EdgeWriteGPLink = "WriteGPLink"
36 EdgeReadLAPSPassword = "ReadLAPSPassword"
37 EdgeReadGMSAPassword = "ReadGMSAPassword"
38)
39
40const (
41 GUIDAllZero = "00000000-0000-0000-0000-000000000000"
42 GUIDUserForceChangePassword = "00299570-246d-11d0-a768-00aa006e0529"
43 GUIDReplicationGetChanges = "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2"
44 GUIDReplicationGetChangesAll = "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2"
45 GUIDReplicationFiltered = "89e95b76-444d-4c62-991a-0facbeda640c"
46 GUIDGroupMemberAttr = "bf9679c0-0de6-11d0-a285-00aa003049e2"
47 GUIDMembershipPropertySet = "bc0ac240-79a9-11d0-9020-00c04fc2d4cf"
48 GUIDAllowedToAct = "3f78c3e5-f79a-46bd-a0b8-9d18116ddc79"
49 GUIDKeyCredentialLink = "5b47d60f-6090-40b2-9f37-2a4de88f3063"
50 GUIDServicePrincipalName = "f3a64788-5306-11d1-a9c5-0000f80367c1"
51 GUIDUserAccountRestrictions = "4c164200-20c0-11d0-a768-00aa006e0529"
52 GUIDWriteGPLink = "f30e3bbe-9ff0-11d1-b603-0000f80367c1"
53
54 GUIDClassUser = "bf967aba-0de6-11d0-a285-00aa003049e2"
55 GUIDClassInetOrg = "ce206244-5827-4a86-ba1c-1c0c386c1b64"
56 GUIDClassGMSA = "7b8b558a-93a5-4af7-adca-c017e67f1057"
57 GUIDClassMSA = "febc6632-080d-422c-a779-c22712b713b0"
58 GUIDClassComputer = "bf967a86-0de6-11d0-a285-00aa003049e2"
59 GUIDClassGroup = "bf967a9c-0de6-11d0-a285-00aa003049e2"
60 GUIDClassDomain = "19195a5a-6da0-11d0-afd3-00c04fd930c9"
61 GUIDClassOU = "bf967aa5-0de6-11d0-a285-00aa003049e2"
62)
63
64func isIgnoredSID(sid string) bool {
65 return sid == "S-1-3-0" || sid == "S-1-5-18" || sid == "S-1-5-10"
66}
67
68func formatGUID(b []byte) string {
69 if len(b) < 16 {
70 return ""
71 }
72 d1 := binary.LittleEndian.Uint32(b[0:4])
73 d2 := binary.LittleEndian.Uint16(b[4:6])
74 d3 := binary.LittleEndian.Uint16(b[6:8])
75 return fmt.Sprintf("%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",
76 d1, d2, d3, b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15])
77}
78
79func inheritedGUIDApplies(inheritedGUID, objectType string) bool {
80 if inheritedGUID == "" || inheritedGUID == GUIDAllZero || objectType == "" {
81 return true
82 }
83 g := strings.ToLower(inheritedGUID)
84 switch strings.ToLower(objectType) {
85 case "user":
86 return g == GUIDClassUser || g == GUIDClassInetOrg || g == GUIDClassGMSA || g == GUIDClassMSA
87 case "computer":
88 return g == GUIDClassComputer || g == GUIDClassGMSA
89 case "group":
90 return g == GUIDClassGroup
91 case "domain":
92 return g == GUIDClassDomain
93 case "ou":
94 return g == GUIDClassOU
95 default:
96 return true
97 }
98}
99
100func ParseSecurityDescriptor(sdBytes []byte) ([]Ace, error) {
101 aces, _, err := ParseSecurityDescriptorForType(sdBytes, "", false, nil)
102 return aces, err
103}
104
105func ParseSecurityDescriptorForType(sdBytes []byte, objectType string, hasLAPS bool, lapsGUIDs map[string]bool) ([]Ace, bool, error) {
106 if len(sdBytes) < 20 {
107 return nil, false, nil
108 }
109
110 control := binary.LittleEndian.Uint16(sdBytes[2:4])
111 isACLProtected := (control & 0x1000) != 0
112
113 type seenKey struct {
114 sid string
115 right string
116 inherited bool
117 }
118 seen := make(map[seenKey]bool)
119 var results []Ace
120
121 addAce := func(sid, right string, inherited bool) {
122 if sid == "" || right == "" {
123 return
124 }
125 k := seenKey{sid: sid, right: right, inherited: inherited}
126 if seen[k] {
127 return
128 }
129 seen[k] = true
130 results = append(results, Ace{
131 PrincipalSID: sid,
132 PrincipalType: "Base",
133 RightName: right,
134 IsInherited: inherited,
135 })
136 }
137
138 objLower := strings.ToLower(objectType)
139
140 ownerOffset := int(binary.LittleEndian.Uint32(sdBytes[4:8]))
141 if objLower != "rbcd" && objLower != "gmsa" && ownerOffset >= 20 && ownerOffset+8 <= len(sdBytes) {
142 ownerSID, _, err := crypto.ParseBinarySID(sdBytes[ownerOffset:])
143 if err == nil && ownerSID != "" && !isIgnoredSID(ownerSID) {
144 addAce(ownerSID, EdgeOwns, false)
145 }
146 }
147
148 if control&0x0004 == 0 {
149 return results, isACLProtected, nil
150 }
151
152 daclOffset := int(binary.LittleEndian.Uint32(sdBytes[16:20]))
153 if daclOffset == 0 || daclOffset >= len(sdBytes) || daclOffset+8 > len(sdBytes) {
154 return results, isACLProtected, nil
155 }
156
157 daclBytes := sdBytes[daclOffset:]
158 aclSize := int(binary.LittleEndian.Uint16(daclBytes[2:4]))
159 aceCount := int(binary.LittleEndian.Uint16(daclBytes[4:6]))
160 if aclSize > len(daclBytes) {
161 aclSize = len(daclBytes)
162 }
163
164 offset := 8
165 for i := 0; i < aceCount && offset+4 <= aclSize; i++ {
166 aceType := daclBytes[offset]
167 aceFlags := daclBytes[offset+1]
168 aceSize := int(binary.LittleEndian.Uint16(daclBytes[offset+2 : offset+4]))
169 if aceSize <= 0 || offset+aceSize > aclSize {
170 break
171 }
172
173 isInherited := (aceFlags & 0x10) != 0
174 isInheritOnly := (aceFlags & 0x08) != 0
175
176 if isInheritOnly {
177 offset += aceSize
178 continue
179 }
180
181 if aceType == 0x00 && aceSize >= 12 {
182 mask := binary.LittleEndian.Uint32(daclBytes[offset+4 : offset+8])
183 sidBytes := daclBytes[offset+8 : offset+aceSize]
184 principalSID, _, err := crypto.ParseBinarySID(sidBytes)
185 if err == nil && principalSID != "" {
186 if objLower == "rbcd" {
187 if mask != 0 {
188 addAce(principalSID, EdgeGenericAll, isInherited)
189 }
190 } else if objLower == "gmsa" {
191 if mask&0x00000100 != 0 || mask&0x10000000 != 0 || mask&0x00020014 != 0 {
192 addAce(principalSID, EdgeReadGMSAPassword, isInherited)
193 }
194 } else if !isIgnoredSID(principalSID) {
195 for _, edge := range mapStandardMaskToEdges(mask, objLower, principalSID) {
196 addAce(principalSID, edge, isInherited)
197 }
198 }
199 }
200 } else if aceType == 0x05 && aceSize >= 12 {
201 mask := binary.LittleEndian.Uint32(daclBytes[offset+4 : offset+8])
202 objFlags := binary.LittleEndian.Uint32(daclBytes[offset+8 : offset+12])
203 sidOffset := offset + 12
204 var objectTypeGUID string
205 var inheritedTypeGUID string
206
207 if objFlags&0x00000001 != 0 {
208 if sidOffset+16 <= offset+aceSize {
209 objectTypeGUID = formatGUID(daclBytes[sidOffset : sidOffset+16])
210 sidOffset += 16
211 }
212 }
213 if objFlags&0x00000002 != 0 {
214 if sidOffset+16 <= offset+aceSize {
215 inheritedTypeGUID = formatGUID(daclBytes[sidOffset : sidOffset+16])
216 sidOffset += 16
217 }
218 }
219
220 if isInherited && (objFlags&0x00000002 != 0) && !inheritedGUIDApplies(inheritedTypeGUID, objLower) {
221 offset += aceSize
222 continue
223 }
224
225 if sidOffset < offset+aceSize {
226 sidBytes := daclBytes[sidOffset : offset+aceSize]
227 principalSID, _, err := crypto.ParseBinarySID(sidBytes)
228 if err == nil && principalSID != "" {
229 if objLower == "rbcd" {
230 if mask != 0 {
231 addAce(principalSID, EdgeGenericAll, isInherited)
232 }
233 } else if objLower == "gmsa" {
234 if mask&0x00000100 != 0 || mask&0x10000000 != 0 || mask&0x00020014 != 0 {
235 addAce(principalSID, EdgeReadGMSAPassword, isInherited)
236 }
237 } else if !isIgnoredSID(principalSID) {
238 for _, edge := range mapObjectMaskToEdges(mask, objFlags, objectTypeGUID, objLower, principalSID, hasLAPS, lapsGUIDs) {
239 addAce(principalSID, edge, isInherited)
240 }
241 }
242 }
243 }
244 }
245
246 offset += aceSize
247 }
248
249 return results, isACLProtected, nil
250}
251
252func hasGenericAll(mask uint32) bool {
253 return mask&0x10000000 != 0 || (mask&0x000F01FF == 0x000F01FF) || (mask&0x001F01FF == 0x001F01FF)
254}
255
256func hasGenericWrite(mask uint32) bool {
257 return mask&0x40000000 != 0 || (mask&0x00020028 == 0x00020028) || mask&0x00000020 != 0
258}
259
260func mapStandardMaskToEdges(mask uint32, objType, principalSID string) []string {
261 var edges []string
262
263 if hasGenericAll(mask) {
264 return []string{EdgeGenericAll}
265 }
266
267 if hasGenericWrite(mask) && objType != "domain" {
268 edges = append(edges, EdgeGenericWrite)
269 }
270 if mask&0x00080000 != 0 {
271 edges = append(edges, EdgeWriteOwner)
272 }
273 if mask&0x00040000 != 0 {
274 edges = append(edges, EdgeWriteDacl)
275 }
276 if mask&0x00000100 != 0 {
277 switch objType {
278 case "", "user", "domain":
279 edges = append(edges, EdgeAllExtendedRights)
280 case "computer":
281 if principalSID != "S-1-5-32-544" && !strings.HasSuffix(principalSID, "-512") {
282 edges = append(edges, EdgeAllExtendedRights)
283 }
284 }
285 }
286
287 return edges
288}
289
290func mapObjectMaskToEdges(mask, objFlags uint32, objectGUID, objType, principalSID string, hasLAPS bool, lapsGUIDs map[string]bool) []string {
291 var edges []string
292
293 objTypePresent := (objFlags & 0x00000001) != 0
294 guidLower := strings.ToLower(objectGUID)
295 genericApplicable := !objTypePresent || guidLower == "" || guidLower == GUIDAllZero
296
297 if genericApplicable {
298 if hasGenericAll(mask) {
299 return []string{EdgeGenericAll}
300 }
301 if hasGenericWrite(mask) {
302 edges = append(edges, EdgeGenericWrite)
303 }
304 if mask&0x00040000 != 0 {
305 edges = append(edges, EdgeWriteDacl)
306 }
307 if mask&0x00080000 != 0 {
308 edges = append(edges, EdgeWriteOwner)
309 }
310 if mask&0x00000100 != 0 {
311 switch objType {
312 case "", "user", "domain":
313 edges = append(edges, EdgeAllExtendedRights)
314 case "computer":
315 if hasLAPS {
316 edges = append(edges, EdgeAllExtendedRights)
317 }
318 }
319 }
320 if (objType == "" || objType == "group") && mask&0x00000008 != 0 {
321 edges = append(edges, EdgeAddSelf)
322 }
323 return edges
324 }
325
326 canWriteProp := mask&0x00000020 != 0 || hasGenericAll(mask) || mask&0x40000000 != 0
327 hasExtRight := mask&0x00000100 != 0 || hasGenericAll(mask)
328
329 switch guidLower {
330 case GUIDGroupMemberAttr, GUIDMembershipPropertySet:
331 if objType == "" || objType == "group" {
332 if canWriteProp {
333 edges = append(edges, EdgeAddMember)
334 } else if mask&0x00000008 != 0 {
335 edges = append(edges, EdgeAddSelf)
336 }
337 }
338 case GUIDUserForceChangePassword:
339 if (objType == "" || objType == "user" || objType == "computer") && hasExtRight {
340 edges = append(edges, EdgeForceChangePassword)
341 }
342 case GUIDReplicationGetChanges:
343 if (objType == "" || objType == "domain") && hasExtRight {
344 edges = append(edges, EdgeGetChanges)
345 }
346 case GUIDReplicationGetChangesAll:
347 if (objType == "" || objType == "domain") && hasExtRight {
348 edges = append(edges, EdgeGetChangesAll)
349 }
350 case GUIDReplicationFiltered:
351 if (objType == "" || objType == "domain") && hasExtRight {
352 edges = append(edges, EdgeGetChangesInFilteredSet)
353 }
354 case GUIDAllowedToAct:
355 if (objType == "" || objType == "computer") && canWriteProp {
356 edges = append(edges, EdgeAddAllowedToAct)
357 }
358 case GUIDKeyCredentialLink:
359 if (objType == "" || objType == "user" || objType == "computer") && canWriteProp {
360 edges = append(edges, EdgeAddKeyCredentialLink)
361 }
362 case GUIDServicePrincipalName:
363 if (objType == "" || objType == "user" || objType == "computer") && canWriteProp {
364 edges = append(edges, EdgeWriteSPN)
365 }
366 case GUIDUserAccountRestrictions:
367 if (objType == "" || objType == "user" || objType == "computer") && canWriteProp && !strings.HasSuffix(principalSID, "-512") {
368 edges = append(edges, EdgeWriteAccountRestrictions)
369 }
370 case GUIDWriteGPLink:
371 if (objType == "" || objType == "ou" || objType == "domain") && canWriteProp {
372 edges = append(edges, EdgeWriteGPLink)
373 }
374 default:
375 if objType == "computer" && hasLAPS && lapsGUIDs != nil && lapsGUIDs[guidLower] && (hasExtRight || mask&0x00000010 != 0) {
376 edges = append(edges, EdgeReadLAPSPassword)
377 }
378 }
379
380 return edges
381}