[SHEHZADE//ABDULLAH ANSARI]
Red Team Consultant @ Google | MS in Cybersecurity @ NYU
● 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
355 lines · 9.9 KB
| 1 | package ad |
| 2 | |
| 3 | import ( |
| 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 | |
| 13 | type wellKnownDef struct { |
| 14 | Name string |
| 15 | Type string |
| 16 | } |
| 17 | |
| 18 | var 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 | |
| 84 | func 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 | |
| 110 | func 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 | |
| 121 | func 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 | |
| 135 | func 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 | |
| 160 | func 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 | |
| 168 | func 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 | |
| 179 | func readOptionalDescription(row *ordereddict.Dict) interface{} { |
| 180 | s := readString(row, "ATTm13") |
| 181 | if s == "" { |
| 182 | return nil |
| 183 | } |
| 184 | return s |
| 185 | } |
| 186 | |
| 187 | func 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 | |
| 218 | func 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 | |
| 234 | func 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 | |
| 244 | func 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 | |
| 256 | func 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 | |
| 274 | func 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 | |
| 285 | func formatGUIDLower(b []byte) string { |
| 286 | return strings.ToLower(formatGUIDUpper(b)) |
| 287 | } |
| 288 | |
| 289 | func 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 | } |