● 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 / ad / assembler.go
← Back to directory
952 lines · 27.0 KB
Raw
1package ad
2
3import (
4 "encoding/binary"
5 "encoding/hex"
6 "fmt"
7 "strings"
8
9 "github.com/Velocidex/ordereddict"
10 "github.com/shehzade/ntds-to-bloodhound/pkg/acl"
11 "github.com/shehzade/ntds-to-bloodhound/pkg/crypto"
12 "github.com/shehzade/ntds-to-bloodhound/pkg/ese"
13)
14
15type Assembler struct {
16 db *ese.DB
17 cryptoEngine *crypto.CryptoEngine
18 secEngine *acl.SecurityEngine
19 tree *Tree
20 Snapshot *ADSnapshot
21 domainOverride string
22
23 groupsByDNT map[uint64]*GroupObject
24 ousByDNT map[uint64]*OUObject
25 deletedDNTs map[uint64]bool
26 domainDNT uint64
27 domainAces []acl.Ace
28 domainACLProtect bool
29 domainFuncLevel string
30 domainDescription interface{}
31 domainTrusts []DomainTrust
32}
33
34func NewAssembler(db *ese.DB, cryptoEngine *crypto.CryptoEngine, secEngine *acl.SecurityEngine, domainOverride string) *Assembler {
35 return &Assembler{
36 db: db,
37 cryptoEngine: cryptoEngine,
38 secEngine: secEngine,
39 tree: NewTree(),
40 Snapshot: NewADSnapshot(),
41 domainOverride: strings.TrimSpace(domainOverride),
42 groupsByDNT: make(map[uint64]*GroupObject),
43 ousByDNT: make(map[uint64]*OUObject),
44 deletedDNTs: make(map[uint64]bool),
45 domainFuncLevel: "2016",
46 domainTrusts: make([]DomainTrust, 0),
47 }
48}
49
50func (a *Assembler) Assemble() error {
51 if err := a.buildTreeAndDomain(); err != nil {
52 return fmt.Errorf("failed to build tree and domain: %w", err)
53 }
54
55 if err := a.extractObjects(); err != nil {
56 return fmt.Errorf("failed to extract objects: %w", err)
57 }
58
59 if err := a.processLinks(); err != nil {
60 return fmt.Errorf("failed to process links: %w", err)
61 }
62
63 a.processPrimaryGroups()
64
65 a.synthesizeWellKnownGroups()
66
67 a.resolvePrincipalsAndAces()
68
69 a.linkContainment()
70
71 return nil
72}
73
74func (a *Assembler) buildTreeAndDomain() error {
75 var fallbackDomainSID string
76 var dcCandidateDNTs []uint64
77
78 err := a.db.StreamTable("datatable", func(row *ordereddict.Dict) error {
79 dnt, ok := readUint64(row, "DNT_col")
80 if !ok || dnt == 0 {
81 return nil
82 }
83 pdnt, _ := readUint64(row, "PDNT_col")
84 a.Snapshot.PDNTByDNT[dnt] = pdnt
85
86 if a.secEngine != nil {
87 ldapName := strings.ToLower(readString(row, "ATTm131532"))
88 if ldapName == "ms-mcs-admpwd" || ldapName == "mslaps-password" || ldapName == "mslaps-encryptedpassword" {
89 if guidHex := readString(row, "ATTk589972"); len(guidHex) == 32 {
90 if gb, err := hex.DecodeString(guidHex); err == nil && len(gb) == 16 {
91 a.secEngine.LapsGUIDs[formatGUIDLower(gb)] = true
92 }
93 }
94 }
95 }
96
97 var rdnCol string = "ATTm3"
98 rdnTyp, hasRDNTyp := readInt64(row, "RDNtyp_col")
99 if !hasRDNTyp {
100 rdnTyp = 3
101 } else {
102 rdnCol = fmt.Sprintf("ATTm%d", rdnTyp)
103 }
104
105 rdn := readString(row, rdnCol)
106 if rdn == "" {
107 rdn = readString(row, "ATTm3")
108 }
109 if rdn == "" {
110 rdn = readString(row, "ATTm589825")
111 }
112 if rdn == "" {
113 rdn = readString(row, "ATTm1376281")
114 }
115 if rdn == "" {
116 rdn = readString(row, "ATTm1376280")
117 }
118
119 if isRowDeleted(row, rdn) {
120 a.deletedDNTs[dnt] = true
121 }
122
123 if sidHex := readString(row, "ATTr589970"); sidHex != "" {
124 if sidBytes, err := hex.DecodeString(sidHex); err == nil {
125 if sidStr, _, err := crypto.ParseSID(sidBytes); err == nil && strings.HasPrefix(sidStr, "S-1-5-21-") {
126 parts := strings.Split(sidStr, "-")
127 if len(parts) == 7 {
128 a.Snapshot.DomainSID = sidStr
129 a.domainDNT = dnt
130 if lvl, hasLvl := readInt64(row, "ATTj591540"); hasLvl {
131 a.domainFuncLevel = functionalLevelName(lvl)
132 }
133 a.domainDescription = readOptionalDescription(row)
134 } else if len(parts) >= 8 && fallbackDomainSID == "" {
135 fallbackDomainSID = strings.Join(parts[:7], "-")
136 }
137 }
138 }
139 }
140
141 if rdn == "" {
142 return nil
143 }
144
145 prefix := "CN="
146 if rdnTyp == 1376281 || strings.HasPrefix(strings.ToLower(rdn), "dc=") {
147 prefix = "DC="
148 dcCandidateDNTs = append(dcCandidateDNTs, dnt)
149 } else if rdnTyp == 1376280 || rdnTyp == 11 || strings.HasPrefix(strings.ToLower(rdn), "ou=") {
150 prefix = "OU="
151 }
152
153 cleanRDN := rdn
154 if strings.HasPrefix(strings.ToUpper(cleanRDN), prefix) {
155 cleanRDN = cleanRDN[len(prefix):]
156 }
157
158 a.tree.AddNode(dnt, pdnt, cleanRDN, prefix)
159 return nil
160 })
161 if err != nil {
162 return err
163 }
164
165 if a.Snapshot.DomainSID == "" && fallbackDomainSID != "" {
166 a.Snapshot.DomainSID = fallbackDomainSID
167 }
168
169 if a.domainDNT != 0 {
170 if dom := domainFromDN(a.tree.ResolveDN(a.domainDNT)); dom != "" {
171 a.Snapshot.DomainName = dom
172 }
173 }
174 if a.Snapshot.DomainName == "" {
175 for _, candDNT := range dcCandidateDNTs {
176 if dom := domainFromDN(a.tree.ResolveDN(candDNT)); dom != "" {
177 a.Snapshot.DomainName = dom
178 if a.domainDNT == 0 {
179 a.domainDNT = candDNT
180 }
181 break
182 }
183 }
184 }
185
186 if a.domainOverride != "" {
187 a.Snapshot.DomainName = strings.ToUpper(a.domainOverride)
188 }
189
190 return nil
191}
192
193func (a *Assembler) extractSDID(row *ordereddict.Dict) (uint64, bool) {
194 if sdVal := readString(row, "ATTp131353"); len(sdVal) == 16 {
195 if b, err := hex.DecodeString(sdVal); err == nil && len(b) == 8 {
196 return binary.LittleEndian.Uint64(b), true
197 }
198 }
199 for _, colName := range []string{"ATTb590606", "ATTb49"} {
200 if v, ok := row.Get(colName); ok && v != nil {
201 switch val := v.(type) {
202 case uint64:
203 return val, true
204 case int64:
205 return uint64(val), true
206 case uint32:
207 return uint64(val), true
208 case int32:
209 return uint64(val), true
210 case string:
211 if len(val) == 16 {
212 if b, err := hex.DecodeString(val); err == nil && len(b) == 8 {
213 return binary.LittleEndian.Uint64(b), true
214 }
215 }
216 }
217 }
218 }
219 return 0, false
220}
221
222func (a *Assembler) resolveObjectSD(sdID uint64, foundSD bool, objType string, hasLAPS bool) ([]acl.Ace, bool) {
223 if a.secEngine == nil || !foundSD {
224 return make([]acl.Ace, 0), false
225 }
226 aces, isProtected := a.secEngine.GetAcesForObject(sdID, objType, hasLAPS)
227 if aces == nil {
228 aces = make([]acl.Ace, 0)
229 }
230 return aces, isProtected
231}
232
233func (a *Assembler) extractObjects() error {
234 return a.db.StreamTable("datatable", func(row *ordereddict.Dict) error {
235 dnt, ok := readUint64(row, "DNT_col")
236 if !ok || dnt == 0 || a.deletedDNTs[dnt] {
237 return nil
238 }
239
240 pdnt, _ := readUint64(row, "PDNT_col")
241 a.Snapshot.PDNTByDNT[dnt] = pdnt
242
243 dn := a.tree.ResolveDN(dnt)
244 dnUpper := strings.ToUpper(dn)
245 if isRowDeleted(row, a.tree.GetRDN(dnt)) || isNonDomainPartitionDN(dnUpper) {
246 return nil
247 }
248
249 var sidStr string
250 var rid uint32
251 if sidHex := readString(row, "ATTr589970"); sidHex != "" {
252 if sidBytes, err := hex.DecodeString(sidHex); err == nil {
253 if s, r, err := crypto.ParseSID(sidBytes); err == nil {
254 sidStr = s
255 rid = r
256 }
257 }
258 }
259
260 sdID, foundSD := a.extractSDID(row)
261
262 if dnt == a.domainDNT || (sidStr != "" && sidStr == a.Snapshot.DomainSID) {
263 a.domainDNT = dnt
264 a.domainAces, a.domainACLProtect = a.resolveObjectSD(sdID, foundSD, "domain", false)
265 }
266
267 if trust, isTrust := extractTrustFromRow(row, a.tree.GetRDN(dnt)); isTrust {
268 a.domainTrusts = append(a.domainTrusts, trust)
269 return nil
270 }
271
272 samVal, _ := row.Get("ATTm590045")
273 if samVal == nil {
274 rdnTyp, hasRDN := readInt64(row, "RDNtyp_col")
275 if !hasRDN {
276 rdnTyp = 3
277 }
278
279 var guidStr string
280 if guidHex := readString(row, "ATTk589826"); len(guidHex) == 32 {
281 if gb, err := hex.DecodeString(guidHex); err == nil && len(gb) == 16 {
282 guidStr = formatGUIDUpper(gb)
283 }
284 }
285
286 if rdnTyp == 1376280 || rdnTyp == 11 || (dnUpper != "" && strings.HasPrefix(dnUpper, "OU=")) {
287 ouName := readString(row, "ATTm11")
288 if ouName == "" {
289 ouName = readString(row, "ATTm1376280")
290 }
291 if ouName == "" {
292 ouName = a.tree.GetRDN(dnt)
293 }
294 if guidStr == "" {
295 guidStr = fmt.Sprintf("OU-%d", dnt)
296 }
297
298 blocksInheritance := false
299 if gpOpt, hasGP := readInt64(row, "ATTj589846"); hasGP && (gpOpt&1) != 0 {
300 blocksInheritance = true
301 } else if readString(row, "ATTm589846") == "1" {
302 blocksInheritance = true
303 }
304
305 aces, isACLProtected := a.resolveObjectSD(sdID, foundSD, "ou", false)
306
307 ouObj := &OUObject{
308 ObjectIdentifier: guidStr,
309 ChildObjects: make([]TypedPrincipal, 0),
310 Links: make([]interface{}, 0),
311 Aces: aces,
312 IsACLProtected: isACLProtected,
313 DNT: dnt,
314 PDNT: pdnt,
315 Properties: map[string]interface{}{
316 "domain": a.Snapshot.DomainName,
317 "name": fmt.Sprintf("%s@%s", strings.ToUpper(ouName), a.Snapshot.DomainName),
318 "distinguishedname": dnUpper,
319 "domainsid": a.Snapshot.DomainSID,
320 "highvalue": false,
321 "objectid": guidStr,
322 "blocksinheritance": blocksInheritance,
323 "isaclprotected": isACLProtected,
324 "description": readOptionalDescription(row),
325 },
326 }
327 a.Snapshot.OUs = append(a.Snapshot.OUs, ouObj)
328 a.Snapshot.OUsByGUID[guidStr] = ouObj
329 a.ousByDNT[dnt] = ouObj
330 a.Snapshot.ObjectsByDNT[dnt] = TypedPrincipal{ObjectIdentifier: guidStr, ObjectType: "OU"}
331 }
332 return nil
333 }
334
335 sam, ok := samVal.(string)
336 if !ok || len(sam) == 0 || sidStr == "" {
337 return nil
338 }
339
340 uacVal, _ := readInt64(row, "ATTj589832")
341 uac := uint32(uacVal)
342
343 samAccountType, _ := readInt64(row, "ATTj590126")
344 if samAccountType == 805306370 || (uac&0x0800) != 0 {
345 return nil
346 }
347
348 var primaryGroupID uint32 = 513
349 pgInt, hasPG := readInt64(row, "ATTj589922")
350 if hasPG {
351 primaryGroupID = uint32(pgInt)
352 }
353
354 acInt, _ := readInt64(row, "ATTj589842")
355 isAdmin := acInt > 0
356
357 var pwdLastSet int64 = -1
358 if plsInt, hasPLS := readInt64(row, "ATTq589983"); hasPLS {
359 pwdLastSet = filetimeToUnix(plsInt)
360 }
361
362 var lastLogonTS int64 = -1
363 if llTSInt, hasLLTS := readInt64(row, "ATTq590204"); hasLLTS {
364 lastLogonTS = filetimeToUnix(llTSInt)
365 }
366
367 var lastLogon int64 = -1
368 if llInt, hasLL := readInt64(row, "ATTq589876"); hasLL {
369 lastLogon = filetimeToUnix(llInt)
370 }
371
372 var ntHashHex string
373 if a.cryptoEngine != nil && len(a.cryptoEngine.PEKList) > 0 && rid > 0 {
374 if pwdVal, ok := row.Get("ATTk589914"); ok && pwdVal != nil {
375 if pwdHex, ok := pwdVal.(string); ok && len(pwdHex) > 0 {
376 if encBytes, err := hex.DecodeString(pwdHex); err == nil {
377 if decHash, err := a.cryptoEngine.DecryptHash(encBytes, rid); err == nil {
378 ntHashHex = hex.EncodeToString(decHash)
379 }
380 }
381 }
382 }
383 }
384
385 spns := readStringSlice(row, "ATTm590595")
386 delegateTargets := readStringSlice(row, "ATTm591788")
387 sidHistory := readESESIDSlice(row, "ATTr590433")
388 if sidHistory == nil {
389 sidHistory = make([]string, 0)
390 }
391 desc := readOptionalDescription(row)
392 gmsaHex := readString(row, "ATTk592202")
393 rbcdHex := readString(row, "ATTk592023")
394
395 _, hasLegacyLAPSExp := readInt64(row, "ATTq591790")
396 _, hasWinLAPSExp := readInt64(row, "ATTq592270")
397 hasLAPS := hasLegacyLAPSExp || hasWinLAPSExp || readString(row, "ATTm591789") != ""
398
399 isGroup := samAccountType == 268435456 || samAccountType == 268435457 ||
400 samAccountType == 536870912 || samAccountType == 536870913 ||
401 (samAccountType == 0 && uac == 0 && !hasPG && ntHashHex == "" && !strings.HasSuffix(sam, "$"))
402
403 isComputer := !isGroup && gmsaHex == "" &&
404 (samAccountType == 805306369 || (samAccountType == 0 && strings.HasSuffix(sam, "$")))
405
406 if isComputer {
407 dnsName := readString(row, "ATTm590443")
408 if dnsName == "" {
409 baseHost := strings.TrimSuffix(sam, "$")
410 dnsName = fmt.Sprintf("%s.%s", baseHost, a.Snapshot.DomainName)
411 }
412
413 osName := readString(row, "ATTm590187")
414 osVer := readString(row, "ATTm590188")
415 isDC := (uac&0x2000) != 0 || primaryGroupID == 516 || strings.Contains(dnUpper, "OU=DOMAIN CONTROLLERS,")
416
417 aces, isACLProtected := a.resolveObjectSD(sdID, foundSD, "computer", hasLAPS)
418
419 var rawAllowedToAct []string
420 if rbcdHex != "" {
421 if rbcdBytes, err := hex.DecodeString(rbcdHex); err == nil {
422 rbcdAces, _, _ := acl.ParseSecurityDescriptorForType(rbcdBytes, "rbcd", false, nil)
423 for _, ra := range rbcdAces {
424 rawAllowedToAct = append(rawAllowedToAct, ra.PrincipalSID)
425 }
426 }
427 }
428
429 comp := &ComputerObject{
430 ObjectIdentifier: sidStr,
431 PrimaryGroupSID: fmt.Sprintf("%s-%d", a.Snapshot.DomainSID, primaryGroupID),
432 AllowedToDelegate: make([]TypedPrincipal, 0),
433 AllowedToAct: make([]TypedPrincipal, 0),
434 HasSIDHistory: make([]TypedPrincipal, 0),
435 DumpSMSAPassword: make([]TypedPrincipal, 0),
436 Sessions: EmptySessionCollection(),
437 PrivilegedSessions: EmptySessionCollection(),
438 RegistrySessions: EmptySessionCollection(),
439 LocalGroups: make([]interface{}, 0),
440 UserRights: make([]interface{}, 0),
441 Aces: aces,
442 IsACLProtected: isACLProtected,
443 IsDC: isDC,
444 DNT: dnt,
445 PDNT: pdnt,
446 NTHash: ntHashHex,
447 RawDelegateTargets: delegateTargets,
448 RawAllowedToAct: rawAllowedToAct,
449 RawSIDHistory: sidHistory,
450 Properties: map[string]interface{}{
451 "domain": a.Snapshot.DomainName,
452 "name": strings.ToUpper(dnsName),
453 "distinguishedname": dnUpper,
454 "domainsid": a.Snapshot.DomainSID,
455 "highvalue": isDC,
456 "objectid": sidStr,
457 "samaccountname": sam,
458 "enabled": (uac & 2) == 0,
459 "haslaps": hasLAPS,
460 "isaclprotected": isACLProtected,
461 "isdc": isDC,
462 "operatingsystem": osName,
463 "operatingsystemversion": osVer,
464 "pwdlastset": pwdLastSet,
465 "lastlogon": lastLogon,
466 "lastlogontimestamp": lastLogonTS,
467 "unconstraineddelegation": (uac & 0x80000) != 0,
468 "trustedtoauth": (uac & 0x1000000) != 0,
469 "serviceprincipalnames": spns,
470 "sidhistory": sidHistory,
471 "description": desc,
472 "nthash": ntHashHex,
473 },
474 }
475 a.Snapshot.Computers = append(a.Snapshot.Computers, comp)
476 a.Snapshot.ComputersBySID[sidStr] = comp
477 a.Snapshot.ObjectsByDNT[dnt] = TypedPrincipal{ObjectIdentifier: sidStr, ObjectType: "Computer"}
478
479 } else if !isGroup {
480 upn := readString(row, "ATTm590480")
481 aces, isACLProtected := a.resolveObjectSD(sdID, foundSD, "user", false)
482
483 if gmsaHex != "" {
484 if gmsaBytes, err := hex.DecodeString(gmsaHex); err == nil {
485 gmsaAces, _, _ := acl.ParseSecurityDescriptorForType(gmsaBytes, "gmsa", false, nil)
486 aces = append(aces, gmsaAces...)
487 }
488 }
489
490 userName := fmt.Sprintf("%s@%s", strings.ToUpper(sam), a.Snapshot.DomainName)
491 user := &UserObject{
492 ObjectIdentifier: sidStr,
493 PrimaryGroupSID: fmt.Sprintf("%s-%d", a.Snapshot.DomainSID, primaryGroupID),
494 AllowedToDelegate: make([]TypedPrincipal, 0),
495 SPNTargets: make([]interface{}, 0),
496 HasSIDHistory: make([]TypedPrincipal, 0),
497 Aces: aces,
498 IsACLProtected: isACLProtected,
499 DNT: dnt,
500 PDNT: pdnt,
501 NTHash: ntHashHex,
502 RawDelegateTargets: delegateTargets,
503 RawSIDHistory: sidHistory,
504 Properties: map[string]interface{}{
505 "domain": a.Snapshot.DomainName,
506 "name": userName,
507 "distinguishedname": dnUpper,
508 "domainsid": a.Snapshot.DomainSID,
509 "highvalue": isAdmin || rid == 500,
510 "objectid": sidStr,
511 "samaccountname": sam,
512 "enabled": (uac & 2) == 0,
513 "pwdlastset": pwdLastSet,
514 "lastlogon": lastLogon,
515 "lastlogontimestamp": lastLogonTS,
516 "userprincipalname": upn,
517 "admincount": isAdmin,
518 "dontreqpreauth": (uac & 0x400000) != 0,
519 "passwordnotreqd": (uac & 0x20) != 0,
520 "pwdneverexpires": (uac & 0x10000) != 0,
521 "sensitive": (uac & 0x100000) != 0,
522 "unconstraineddelegation": (uac & 0x80000) != 0,
523 "trustedtoauth": (uac & 0x1000000) != 0,
524 "isaclprotected": isACLProtected,
525 "hasspn": len(spns) > 0,
526 "serviceprincipalnames": spns,
527 "sidhistory": sidHistory,
528 "haslaps": false,
529 "description": desc,
530 "nthash": ntHashHex,
531 },
532 }
533 a.Snapshot.Users = append(a.Snapshot.Users, user)
534 a.Snapshot.UsersBySID[sidStr] = user
535 a.Snapshot.ObjectsByDNT[dnt] = TypedPrincipal{ObjectIdentifier: sidStr, ObjectType: "User"}
536
537 } else {
538 aces, isACLProtected := a.resolveObjectSD(sdID, foundSD, "group", false)
539 objID := sidStr
540 if _, isWK := wellKnownSIDs[sidStr]; isWK && a.Snapshot.DomainName != "" {
541 objID = fmt.Sprintf("%s-%s", a.Snapshot.DomainName, sidStr)
542 }
543
544 groupName := fmt.Sprintf("%s@%s", strings.ToUpper(sam), a.Snapshot.DomainName)
545 grp := &GroupObject{
546 ObjectIdentifier: objID,
547 Members: make([]MemberReference, 0),
548 Aces: aces,
549 IsACLProtected: isACLProtected,
550 DNT: dnt,
551 PDNT: pdnt,
552 Properties: map[string]interface{}{
553 "domain": a.Snapshot.DomainName,
554 "name": groupName,
555 "distinguishedname": dnUpper,
556 "domainsid": a.Snapshot.DomainSID,
557 "highvalue": isAdmin || isHighValueGroupSID(sidStr, rid),
558 "objectid": objID,
559 "samaccountname": sam,
560 "admincount": isAdmin,
561 "isaclprotected": isACLProtected,
562 "description": desc,
563 },
564 }
565 a.Snapshot.Groups = append(a.Snapshot.Groups, grp)
566 a.Snapshot.GroupsBySID[objID] = grp
567 if objID != sidStr {
568 a.Snapshot.GroupsBySID[sidStr] = grp
569 }
570 a.groupsByDNT[dnt] = grp
571 a.Snapshot.ObjectsByDNT[dnt] = TypedPrincipal{ObjectIdentifier: objID, ObjectType: "Group"}
572 }
573
574 return nil
575 })
576}
577
578func (a *Assembler) processLinks() error {
579 return a.db.StreamTable("link_table", func(row *ordereddict.Dict) error {
580 linkBaseVal, ok := row.Get("link_base")
581 if !ok || linkBaseVal == nil {
582 return nil
583 }
584
585 var linkBase uint32
586 switch v := linkBaseVal.(type) {
587 case int32:
588 linkBase = uint32(v)
589 case uint32:
590 linkBase = v
591 case int64:
592 linkBase = uint32(v)
593 case uint64:
594 linkBase = uint32(v)
595 }
596 if linkBase != 1 {
597 return nil
598 }
599
600 linkDNTVal, _ := row.Get("link_DNT")
601 backlinkDNTVal, _ := row.Get("backlink_DNT")
602 if linkDNTVal == nil || backlinkDNTVal == nil {
603 return nil
604 }
605
606 var groupDNT, memberDNT uint64
607 switch v := linkDNTVal.(type) {
608 case uint64:
609 groupDNT = v
610 case int64:
611 groupDNT = uint64(v)
612 case int32:
613 groupDNT = uint64(v)
614 case uint32:
615 groupDNT = uint64(v)
616 }
617 switch v := backlinkDNTVal.(type) {
618 case uint64:
619 memberDNT = v
620 case int64:
621 memberDNT = uint64(v)
622 case int32:
623 memberDNT = uint64(v)
624 case uint32:
625 memberDNT = uint64(v)
626 }
627
628 grp, exists := a.groupsByDNT[groupDNT]
629 if !exists {
630 return nil
631 }
632
633 targetPrincipal, exists := a.Snapshot.ObjectsByDNT[memberDNT]
634 if !exists || targetPrincipal.ObjectIdentifier == "" {
635 return nil
636 }
637
638 for _, m := range grp.Members {
639 if m.ObjectIdentifier == targetPrincipal.ObjectIdentifier {
640 return nil
641 }
642 }
643
644 grp.Members = append(grp.Members, MemberReference{
645 ObjectIdentifier: targetPrincipal.ObjectIdentifier,
646 ObjectType: targetPrincipal.ObjectType,
647 })
648
649 return nil
650 })
651}
652
653func (a *Assembler) processPrimaryGroups() {
654 for _, u := range a.Snapshot.Users {
655 if grp, exists := a.Snapshot.GroupsBySID[u.PrimaryGroupSID]; exists {
656 alreadyMember := false
657 for _, m := range grp.Members {
658 if m.ObjectIdentifier == u.ObjectIdentifier {
659 alreadyMember = true
660 break
661 }
662 }
663 if !alreadyMember {
664 grp.Members = append(grp.Members, MemberReference{
665 ObjectIdentifier: u.ObjectIdentifier,
666 ObjectType: "User",
667 })
668 }
669 }
670 }
671
672 for _, c := range a.Snapshot.Computers {
673 if grp, exists := a.Snapshot.GroupsBySID[c.PrimaryGroupSID]; exists {
674 alreadyMember := false
675 for _, m := range grp.Members {
676 if m.ObjectIdentifier == c.ObjectIdentifier {
677 alreadyMember = true
678 break
679 }
680 }
681 if !alreadyMember {
682 grp.Members = append(grp.Members, MemberReference{
683 ObjectIdentifier: c.ObjectIdentifier,
684 ObjectType: "Computer",
685 })
686 }
687 }
688 }
689
690 if a.Snapshot.DomainSID != "" {
691 if a.domainAces == nil {
692 a.domainAces = make([]acl.Ace, 0)
693 }
694 domDN := fmt.Sprintf("DC=%s", strings.ReplaceAll(strings.ToUpper(a.Snapshot.DomainName), ".", ",DC="))
695 domObj := &DomainObject{
696 ObjectIdentifier: a.Snapshot.DomainSID,
697 ChildObjects: make([]TypedPrincipal, 0),
698 Trusts: a.domainTrusts,
699 Links: make([]interface{}, 0),
700 Aces: a.domainAces,
701 IsACLProtected: a.domainACLProtect,
702 DNT: a.domainDNT,
703 Properties: map[string]interface{}{
704 "domain": a.Snapshot.DomainName,
705 "name": a.Snapshot.DomainName,
706 "distinguishedname": domDN,
707 "domainsid": a.Snapshot.DomainSID,
708 "highvalue": true,
709 "objectid": a.Snapshot.DomainSID,
710 "functionallevel": a.domainFuncLevel,
711 "isaclprotected": a.domainACLProtect,
712 "collected": true,
713 "description": a.domainDescription,
714 },
715 }
716 a.Snapshot.Domains = append(a.Snapshot.Domains, domObj)
717 }
718}
719
720func (a *Assembler) synthesizeWellKnownGroups() {
721 if a.Snapshot.DomainName == "" {
722 return
723 }
724
725 defaults := []struct {
726 rawSID string
727 name string
728 }{
729 {rawSID: "S-1-1-0", name: "EVERYONE"},
730 {rawSID: "S-1-5-11", name: "AUTHENTICATED USERS"},
731 {rawSID: "S-1-5-4", name: "INTERACTIVE"},
732 {rawSID: "S-1-5-9", name: "ENTERPRISE DOMAIN CONTROLLERS"},
733 }
734
735 for _, def := range defaults {
736 prefixedID := fmt.Sprintf("%s-%s", a.Snapshot.DomainName, def.rawSID)
737 if _, exists := a.Snapshot.GroupsBySID[prefixedID]; exists {
738 continue
739 }
740 if _, exists := a.Snapshot.GroupsBySID[def.rawSID]; exists {
741 continue
742 }
743
744 members := make([]MemberReference, 0)
745 if def.rawSID == "S-1-5-9" {
746 for _, comp := range a.Snapshot.Computers {
747 if comp.IsDC {
748 members = append(members, MemberReference{
749 ObjectIdentifier: comp.ObjectIdentifier,
750 ObjectType: "Computer",
751 })
752 }
753 }
754 }
755
756 grp := &GroupObject{
757 ObjectIdentifier: prefixedID,
758 Members: members,
759 Aces: make([]acl.Ace, 0),
760 Synthetic: true,
761 Properties: map[string]interface{}{
762 "domain": a.Snapshot.DomainName,
763 "domainsid": a.Snapshot.DomainSID,
764 "name": fmt.Sprintf("%s@%s", def.name, a.Snapshot.DomainName),
765 "objectid": prefixedID,
766 "highvalue": false,
767 },
768 }
769 a.Snapshot.Groups = append(a.Snapshot.Groups, grp)
770 a.Snapshot.GroupsBySID[prefixedID] = grp
771 a.Snapshot.GroupsBySID[def.rawSID] = grp
772 }
773}
774
775func (a *Assembler) resolveSID(rawSID string) (string, string) {
776 if rawSID == "" {
777 return "", "Base"
778 }
779 if rawSID == a.Snapshot.DomainSID {
780 return rawSID, "Domain"
781 }
782 if u, ok := a.Snapshot.UsersBySID[rawSID]; ok {
783 return u.ObjectIdentifier, "User"
784 }
785 if c, ok := a.Snapshot.ComputersBySID[rawSID]; ok {
786 return c.ObjectIdentifier, "Computer"
787 }
788 if g, ok := a.Snapshot.GroupsBySID[rawSID]; ok {
789 return g.ObjectIdentifier, "Group"
790 }
791 if wk, ok := wellKnownSIDs[rawSID]; ok && a.Snapshot.DomainName != "" {
792 return fmt.Sprintf("%s-%s", a.Snapshot.DomainName, rawSID), wk.Type
793 }
794 return rawSID, "Base"
795}
796
797func (a *Assembler) resolveDelegateSPNs(spns []string) []TypedPrincipal {
798 out := make([]TypedPrincipal, 0)
799 if len(spns) == 0 {
800 return out
801 }
802
803 compByHost := make(map[string]*ComputerObject)
804 for _, c := range a.Snapshot.Computers {
805 if nameStr, ok := c.Properties["name"].(string); ok && nameStr != "" {
806 upper := strings.ToUpper(nameStr)
807 compByHost[upper] = c
808 if idx := strings.IndexByte(upper, '.'); idx > 0 {
809 compByHost[upper[:idx]] = c
810 }
811 }
812 if samStr, ok := c.Properties["samaccountname"].(string); ok && samStr != "" {
813 short := strings.ToUpper(strings.TrimSuffix(samStr, "$"))
814 compByHost[short] = c
815 }
816 }
817
818 seen := make(map[string]bool)
819 for _, spn := range spns {
820 parts := strings.SplitN(spn, "/", 2)
821 if len(parts) < 2 {
822 continue
823 }
824 host := strings.ToUpper(parts[1])
825 if idx := strings.IndexByte(host, ':'); idx > 0 {
826 host = host[:idx]
827 }
828 if idx := strings.IndexByte(host, '/'); idx > 0 {
829 host = host[:idx]
830 }
831 comp, ok := compByHost[host]
832 if !ok {
833 if dot := strings.IndexByte(host, '.'); dot > 0 {
834 comp, ok = compByHost[host[:dot]]
835 }
836 }
837 if ok && comp != nil && !seen[comp.ObjectIdentifier] {
838 seen[comp.ObjectIdentifier] = true
839 out = append(out, TypedPrincipal{
840 ObjectIdentifier: comp.ObjectIdentifier,
841 ObjectType: "Computer",
842 })
843 }
844 }
845 return out
846}
847
848func (a *Assembler) resolveSIDList(rawSIDs []string) []TypedPrincipal {
849 out := make([]TypedPrincipal, 0, len(rawSIDs))
850 seen := make(map[string]bool)
851 for _, s := range rawSIDs {
852 id, pType := a.resolveSID(s)
853 if id != "" && !seen[id] {
854 seen[id] = true
855 out = append(out, TypedPrincipal{
856 ObjectIdentifier: id,
857 ObjectType: pType,
858 })
859 }
860 }
861 return out
862}
863
864func (a *Assembler) rewriteAces(aces []acl.Ace) {
865 for i := range aces {
866 aces[i].PrincipalSID, aces[i].PrincipalType = a.resolveSID(aces[i].PrincipalSID)
867 }
868}
869
870func (a *Assembler) resolvePrincipalsAndAces() {
871 for _, u := range a.Snapshot.Users {
872 a.rewriteAces(u.Aces)
873 u.AllowedToDelegate = a.resolveDelegateSPNs(u.RawDelegateTargets)
874 u.HasSIDHistory = a.resolveSIDList(u.RawSIDHistory)
875 }
876 for _, g := range a.Snapshot.Groups {
877 a.rewriteAces(g.Aces)
878 }
879 for _, c := range a.Snapshot.Computers {
880 a.rewriteAces(c.Aces)
881 c.AllowedToDelegate = a.resolveDelegateSPNs(c.RawDelegateTargets)
882 c.AllowedToAct = a.resolveSIDList(c.RawAllowedToAct)
883 c.HasSIDHistory = a.resolveSIDList(c.RawSIDHistory)
884 }
885 for _, d := range a.Snapshot.Domains {
886 a.rewriteAces(d.Aces)
887 }
888 for _, o := range a.Snapshot.OUs {
889 a.rewriteAces(o.Aces)
890 }
891}
892
893func (a *Assembler) linkContainment() {
894 var rootDom *DomainObject
895 if len(a.Snapshot.Domains) > 0 {
896 rootDom = a.Snapshot.Domains[0]
897 }
898
899 addChild := func(pdnt uint64, childID string, childType string) {
900 if childID == "" {
901 return
902 }
903 tp := TypedPrincipal{
904 ObjectIdentifier: childID,
905 ObjectType: childType,
906 }
907
908 curr := pdnt
909 visited := make(map[uint64]bool)
910 for curr != 0 && !visited[curr] {
911 visited[curr] = true
912 if ou, ok := a.ousByDNT[curr]; ok {
913 ou.ChildObjects = append(ou.ChildObjects, tp)
914 return
915 }
916 if a.domainDNT != 0 && curr == a.domainDNT {
917 if rootDom != nil {
918 rootDom.ChildObjects = append(rootDom.ChildObjects, tp)
919 }
920 return
921 }
922 next, exists := a.Snapshot.PDNTByDNT[curr]
923 if !exists || next == curr {
924 break
925 }
926 curr = next
927 }
928
929 if a.domainDNT == 0 && rootDom != nil {
930 rootDom.ChildObjects = append(rootDom.ChildObjects, tp)
931 }
932 }
933
934 for _, ou := range a.Snapshot.OUs {
935 addChild(ou.PDNT, ou.ObjectIdentifier, "OU")
936 }
937
938 for _, u := range a.Snapshot.Users {
939 addChild(u.PDNT, u.ObjectIdentifier, "User")
940 }
941
942 for _, c := range a.Snapshot.Computers {
943 addChild(c.PDNT, c.ObjectIdentifier, "Computer")
944 }
945
946 for _, g := range a.Snapshot.Groups {
947 if g.Synthetic {
948 continue
949 }
950 addChild(g.PDNT, g.ObjectIdentifier, "Group")
951 }
952}