● 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 / helpers.go
← Back to directory
355 lines · 9.9 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/crypto"
11)
12
13type wellKnownDef struct {
14 Name string
15 Type string
16}
17
18var wellKnownSIDs = map[string]wellKnownDef{
19 "S-1-0": {Name: "Null Authority", Type: "User"},
20 "S-1-0-0": {Name: "Nobody", Type: "User"},
21 "S-1-1": {Name: "World Authority", Type: "User"},
22 "S-1-1-0": {Name: "Everyone", Type: "Group"},
23 "S-1-2": {Name: "Local Authority", Type: "User"},
24 "S-1-2-0": {Name: "Local", Type: "Group"},
25 "S-1-2-1": {Name: "Console Logon", Type: "Group"},
26 "S-1-3": {Name: "Creator Authority", Type: "User"},
27 "S-1-3-0": {Name: "Creator Owner", Type: "User"},
28 "S-1-3-1": {Name: "Creator Group", Type: "Group"},
29 "S-1-3-2": {Name: "Creator Owner Server", Type: "Computer"},
30 "S-1-3-3": {Name: "Creator Group Server", Type: "Computer"},
31 "S-1-3-4": {Name: "Owner Rights", Type: "Group"},
32 "S-1-4": {Name: "Non-unique Authority", Type: "User"},
33 "S-1-5": {Name: "NT Authority", Type: "User"},
34 "S-1-5-1": {Name: "Dialup", Type: "Group"},
35 "S-1-5-2": {Name: "Network", Type: "Group"},
36 "S-1-5-3": {Name: "Batch", Type: "Group"},
37 "S-1-5-4": {Name: "Interactive", Type: "Group"},
38 "S-1-5-6": {Name: "Service", Type: "Group"},
39 "S-1-5-7": {Name: "Anonymous", Type: "Group"},
40 "S-1-5-8": {Name: "Proxy", Type: "Group"},
41 "S-1-5-9": {Name: "Enterprise Domain Controllers", Type: "Group"},
42 "S-1-5-10": {Name: "Principal Self", Type: "User"},
43 "S-1-5-11": {Name: "Authenticated Users", Type: "Group"},
44 "S-1-5-12": {Name: "Restricted Code", Type: "Group"},
45 "S-1-5-13": {Name: "Terminal Server Users", Type: "Group"},
46 "S-1-5-14": {Name: "Remote Interactive Logon", Type: "Group"},
47 "S-1-5-15": {Name: "This Organization", Type: "Group"},
48 "S-1-5-17": {Name: "IUSR", Type: "User"},
49 "S-1-5-18": {Name: "Local System", Type: "User"},
50 "S-1-5-19": {Name: "NT Authority", Type: "User"},
51 "S-1-5-20": {Name: "Network Service", Type: "User"},
52 "S-1-5-80-0": {Name: "All Services", Type: "Group"},
53 "S-1-5-32-544": {Name: "Administrators", Type: "Group"},
54 "S-1-5-32-545": {Name: "Users", Type: "Group"},
55 "S-1-5-32-546": {Name: "Guests", Type: "Group"},
56 "S-1-5-32-547": {Name: "Power Users", Type: "Group"},
57 "S-1-5-32-548": {Name: "Account Operators", Type: "Group"},
58 "S-1-5-32-549": {Name: "Server Operators", Type: "Group"},
59 "S-1-5-32-550": {Name: "Print Operators", Type: "Group"},
60 "S-1-5-32-551": {Name: "Backup Operators", Type: "Group"},
61 "S-1-5-32-552": {Name: "Replicators", Type: "Group"},
62 "S-1-5-32-554": {Name: "Pre-Windows 2000 Compatible Access", Type: "Group"},
63 "S-1-5-32-555": {Name: "Remote Desktop Users", Type: "Group"},
64 "S-1-5-32-556": {Name: "Network Configuration Operators", Type: "Group"},
65 "S-1-5-32-557": {Name: "Incoming Forest Trust Builders", Type: "Group"},
66 "S-1-5-32-558": {Name: "Performance Monitor Users", Type: "Group"},
67 "S-1-5-32-559": {Name: "Performance Log Users", Type: "Group"},
68 "S-1-5-32-560": {Name: "Windows Authorization Access Group", Type: "Group"},
69 "S-1-5-32-561": {Name: "Terminal Server License Servers", Type: "Group"},
70 "S-1-5-32-562": {Name: "Distributed COM Users", Type: "Group"},
71 "S-1-5-32-568": {Name: "IIS_IUSRS", Type: "Group"},
72 "S-1-5-32-569": {Name: "Cryptographic Operators", Type: "Group"},
73 "S-1-5-32-573": {Name: "Event Log Readers", Type: "Group"},
74 "S-1-5-32-574": {Name: "Certificate Service DCOM Access", Type: "Group"},
75 "S-1-5-32-575": {Name: "RDS Remote Access Servers", Type: "Group"},
76 "S-1-5-32-576": {Name: "RDS Endpoint Servers", Type: "Group"},
77 "S-1-5-32-577": {Name: "RDS Management Servers", Type: "Group"},
78 "S-1-5-32-578": {Name: "Hyper-V Administrators", Type: "Group"},
79 "S-1-5-32-579": {Name: "Access Control Assistance Operators", Type: "Group"},
80 "S-1-5-32-580": {Name: "Remote Management Users", Type: "Group"},
81 "S-1-5-32-582": {Name: "Storage Replica Administrators", Type: "Group"},
82}
83
84func functionalLevelName(level int64) string {
85 switch level {
86 case 0:
87 return "2000 Mixed/Native"
88 case 1:
89 return "2003 Interim"
90 case 2:
91 return "2003"
92 case 3:
93 return "2008"
94 case 4:
95 return "2008 R2"
96 case 5:
97 return "2012"
98 case 6:
99 return "2012 R2"
100 case 7:
101 return "2016"
102 default:
103 if level > 7 {
104 return "2016"
105 }
106 return "Unknown"
107 }
108}
109
110func isHighValueGroupSID(rawSID string, rid uint32) bool {
111 if rid == 512 || rid == 516 || rid == 519 {
112 return true
113 }
114 switch rawSID {
115 case "S-1-5-32-544", "S-1-5-32-548", "S-1-5-32-549", "S-1-5-32-550", "S-1-5-32-551":
116 return true
117 }
118 return false
119}
120
121func filetimeToUnix(ft int64) int64 {
122 if ft <= 0 || ft == 0x7FFFFFFFFFFFFFFF {
123 if ft == 0 {
124 return 0
125 }
126 return -1
127 }
128 const epochDiff = int64(116444736000000000)
129 if ft < epochDiff {
130 return ft
131 }
132 return (ft - epochDiff) / 10000000
133}
134
135func readInt64(row *ordereddict.Dict, col string) (int64, bool) {
136 val, ok := row.Get(col)
137 if !ok || val == nil {
138 return 0, false
139 }
140 switch v := val.(type) {
141 case int64:
142 return v, true
143 case uint64:
144 return int64(v), true
145 case int32:
146 return int64(v), true
147 case uint32:
148 return int64(v), true
149 case int:
150 return int64(v), true
151 case bool:
152 if v {
153 return 1, true
154 }
155 return 0, true
156 }
157 return 0, false
158}
159
160func readUint64(row *ordereddict.Dict, col string) (uint64, bool) {
161 v, ok := readInt64(row, col)
162 if !ok {
163 return 0, false
164 }
165 return uint64(v), true
166}
167
168func readString(row *ordereddict.Dict, col string) string {
169 val, ok := row.Get(col)
170 if !ok || val == nil {
171 return ""
172 }
173 if s, ok := val.(string); ok {
174 return s
175 }
176 return ""
177}
178
179func readOptionalDescription(row *ordereddict.Dict) interface{} {
180 s := readString(row, "ATTm13")
181 if s == "" {
182 return nil
183 }
184 return s
185}
186
187func readStringSlice(row *ordereddict.Dict, col string) []string {
188 val, ok := row.Get(col)
189 if !ok || val == nil {
190 return make([]string, 0)
191 }
192 switch v := val.(type) {
193 case string:
194 if v == "" {
195 return make([]string, 0)
196 }
197 return []string{v}
198 case []string:
199 out := make([]string, 0, len(v))
200 for _, item := range v {
201 if item != "" {
202 out = append(out, item)
203 }
204 }
205 return out
206 case []interface{}:
207 out := make([]string, 0, len(v))
208 for _, item := range v {
209 if s, ok := item.(string); ok && s != "" {
210 out = append(out, s)
211 }
212 }
213 return out
214 }
215 return make([]string, 0)
216}
217
218func readESESIDSlice(row *ordereddict.Dict, col string) []string {
219 rawVals := readStringSlice(row, col)
220 if len(rawVals) == 0 {
221 return nil
222 }
223 var out []string
224 for _, hexStr := range rawVals {
225 if b, err := hex.DecodeString(hexStr); err == nil {
226 if s, _, err := crypto.ParseSID(b); err == nil && s != "" {
227 out = append(out, s)
228 }
229 }
230 }
231 return out
232}
233
234func isRowDeleted(row *ordereddict.Dict, rdn string) bool {
235 if delVal, ok := readInt64(row, "ATTb589872"); ok && delVal != 0 {
236 return true
237 }
238 if strings.Contains(rdn, "\nDEL:") || strings.Contains(rdn, "\\0ADEL:") {
239 return true
240 }
241 return false
242}
243
244func isNonDomainPartitionDN(dnUpper string) bool {
245 if dnUpper == "" {
246 return false
247 }
248 return strings.Contains(dnUpper, "CN=DELETED OBJECTS,") ||
249 strings.Contains(dnUpper, "\nDEL:") ||
250 strings.Contains(dnUpper, "CN=SCHEMA,") ||
251 strings.Contains(dnUpper, "CN=CONFIGURATION,") ||
252 strings.Contains(dnUpper, "DC=DOMAINDNSZONES,") ||
253 strings.Contains(dnUpper, "DC=FORESTDNSZONES,")
254}
255
256func domainFromDN(dn string) string {
257 var dcParts []string
258 for _, part := range strings.Split(dn, ",") {
259 p := strings.TrimSpace(part)
260 if len(p) > 3 && strings.EqualFold(p[:3], "DC=") {
261 comp := p[3:]
262 if strings.EqualFold(comp, "DomainDnsZones") || strings.EqualFold(comp, "ForestDnsZones") {
263 return ""
264 }
265 dcParts = append(dcParts, comp)
266 }
267 }
268 if len(dcParts) == 0 {
269 return ""
270 }
271 return strings.ToUpper(strings.Join(dcParts, "."))
272}
273
274func formatGUIDUpper(b []byte) string {
275 if len(b) < 16 {
276 return ""
277 }
278 return fmt.Sprintf("%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X",
279 binary.LittleEndian.Uint32(b[0:4]),
280 binary.LittleEndian.Uint16(b[4:6]),
281 binary.LittleEndian.Uint16(b[6:8]),
282 b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15])
283}
284
285func formatGUIDLower(b []byte) string {
286 return strings.ToLower(formatGUIDUpper(b))
287}
288
289func extractTrustFromRow(row *ordereddict.Dict, fallbackName string) (DomainTrust, bool) {
290 partner := readString(row, "ATTm590362")
291 dirVal, hasDir := readInt64(row, "ATTj589956")
292 typeVal, hasType := readInt64(row, "ATTj589960")
293 if partner == "" && (!hasDir || !hasType) {
294 return DomainTrust{}, false
295 }
296 if partner == "" {
297 partner = readString(row, "ATTm590359")
298 }
299 if partner == "" {
300 partner = fallbackName
301 }
302 if partner == "" {
303 return DomainTrust{}, false
304 }
305
306 attrVal, _ := readInt64(row, "ATTj590294")
307 var targetSID string
308 if sidHex := readString(row, "ATTr589953"); sidHex != "" {
309 if sb, err := hex.DecodeString(sidHex); err == nil {
310 if s, _, err := crypto.ParseSID(sb); err == nil {
311 targetSID = s
312 }
313 }
314 }
315
316 nonTransitive := (attrVal & 0x00000001) != 0
317 quarantined := (attrVal & 0x00000004) != 0
318 forestTransitive := (attrVal & 0x00000008) != 0
319 crossOrg := (attrVal & 0x00000010) != 0
320 withinForest := (attrVal & 0x00000020) != 0
321 treatAsExternal := (attrVal & 0x00000040) != 0
322
323 bhType := 4
324 isTransitive := false
325 sidFiltering := true
326
327 if withinForest {
328 bhType = 0
329 isTransitive = true
330 sidFiltering = quarantined
331 } else if forestTransitive {
332 bhType = 2
333 isTransitive = true
334 sidFiltering = !treatAsExternal
335 } else if treatAsExternal || crossOrg || quarantined {
336 bhType = 3
337 isTransitive = false
338 sidFiltering = true
339 } else {
340 if typeVal == 1 || typeVal == 2 {
341 bhType = 3
342 }
343 isTransitive = !nonTransitive
344 sidFiltering = true
345 }
346
347 return DomainTrust{
348 TargetDomainName: strings.ToUpper(partner),
349 TargetDomainSid: targetSID,
350 IsTransitive: isTransitive,
351 TrustDirection: int(dirVal),
352 TrustType: bhType,
353 SidFilteringEnabled: sidFiltering,
354 }, true
355}