[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
381 lines · 11.9 KB
| 1 | package acl |
| 2 | |
| 3 | import ( |
| 4 | "encoding/binary" |
| 5 | "fmt" |
| 6 | "strings" |
| 7 | |
| 8 | "github.com/shehzade/ntds-to-bloodhound/pkg/crypto" |
| 9 | ) |
| 10 | |
| 11 | type Ace struct { |
| 12 | PrincipalSID string `json:"PrincipalSID"` |
| 13 | PrincipalType string `json:"PrincipalType"` |
| 14 | RightName string `json:"RightName"` |
| 15 | IsInherited bool `json:"IsInherited"` |
| 16 | } |
| 17 | |
| 18 | const ( |
| 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 | |
| 40 | const ( |
| 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 | |
| 64 | func isIgnoredSID(sid string) bool { |
| 65 | return sid == "S-1-3-0" || sid == "S-1-5-18" || sid == "S-1-5-10" |
| 66 | } |
| 67 | |
| 68 | func 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 | |
| 79 | func 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 | |
| 100 | func ParseSecurityDescriptor(sdBytes []byte) ([]Ace, error) { |
| 101 | aces, _, err := ParseSecurityDescriptorForType(sdBytes, "", false, nil) |
| 102 | return aces, err |
| 103 | } |
| 104 | |
| 105 | func 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 | |
| 252 | func hasGenericAll(mask uint32) bool { |
| 253 | return mask&0x10000000 != 0 || (mask&0x000F01FF == 0x000F01FF) || (mask&0x001F01FF == 0x001F01FF) |
| 254 | } |
| 255 | |
| 256 | func hasGenericWrite(mask uint32) bool { |
| 257 | return mask&0x40000000 != 0 || (mask&0x00020028 == 0x00020028) || mask&0x00000020 != 0 |
| 258 | } |
| 259 | |
| 260 | func 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 | |
| 290 | func 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 | } |