├── .gitignore ├── README.md ├── ldd2bh.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | sample.data/* 2 | scratch.data/* 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ldd2bh 2 | 3 | ## Usage 4 | 5 | This currently only works for Bloodhound versions 4.0 and below. A workaround would be to import using an old version, then open a newer version, and keep working from there. The ADCS imports from Certipy were tested and does work with the workaround. 6 | 7 | ``` 8 | usage: ldd2bh.py [-h] [-i INPUT_FOLDER] [-o OUTPUT_FOLDER] [-a] [-u] [-c] [-g] 9 | [-d] 10 | 11 | Convert ldapdomaindump to Bloodhound 12 | 13 | optional arguments: 14 | -h, --help show this help message and exit 15 | -i INPUT_FOLDER, --input INPUT_FOLDER 16 | Input Directory for ldapdomaindump data, default: 17 | current directory 18 | -o OUTPUT_FOLDER, --output OUTPUT_FOLDER 19 | Output Directory for Bloodhound data, default: current 20 | directory 21 | -a, --all Output all files, default: True 22 | -u, --users Output only users, default: False 23 | -c, --computers Output only computers, default: False 24 | -g, --groups Output only groups, default: False 25 | -d, --domains Output only domains, default: False 26 | 27 | Examples: 28 | python3 ldd2bh.py -i ldd -o bh 29 | ``` 30 | 31 | ## TODO 32 | - [x] Parse `domain_users.json` 33 | - [x] Fix itermittent bug where `users.json` needs to be pretty printed to upload properly 34 | - [x] Parse `domain_computers.json` 35 | - [x] Parse `domain_groups.json` 36 | - [x] Parse `domain_policy.json` 37 | - [x] Parse `domain_trusts.json` 38 | - [x] Fix non-working domain trusts 39 | - [ ] Double check there isn't more information included for local admin rights 40 | - [ ] Double check any other information that could be helpful or was accidentally skipped 41 | - [ ] Code cleanup 42 | -------------------------------------------------------------------------------- /ldd2bh.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os, sys, uuid, argparse, textwrap, glob, json, base64, re 4 | from datetime import datetime 5 | from binascii import b2a_hex 6 | 7 | hvt = ["512", "516", "519", "520"] 8 | 9 | db = {} 10 | 11 | # https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties 12 | user_access_control = { 13 | "SCRIPT": 0x0001, 14 | "ACCOUNTDISABLE": 0x0002, 15 | "HOMEDIR_REQUIRED": 0x0008, 16 | "LOCKOUT": 0x0010, 17 | "PASSWD_NOTREQD": 0x0020, 18 | "PASSWD_CANT_CHANGE": 0x0040, 19 | "ENCRYPTED_TEXT_PWD_ALLOWED": 0x0080, 20 | "TEMP_DUPLICATE_ACCOUNT": 0x0100, 21 | "NORMAL_ACCOUNT": 0x0200, 22 | "INTERDOMAIN_TRUST_ACCOUNT": 0x0800, 23 | "WORKSTATION_TRUST_ACCOUNT": 0x1000, 24 | "SERVER_TRUST_ACCOUNT": 0x2000, 25 | "DONT_EXPIRE_PASSWORD": 0x10000, 26 | "MNS_LOGON_ACCOUNT": 0x20000, 27 | "SMARTCARD_REQUIRED": 0x40000, 28 | "TRUSTED_FOR_DELEGATION": 0x80000, 29 | "NOT_DELEGATED": 0x100000, 30 | "USE_DES_KEY_ONLY": 0x200000, 31 | "DONT_REQ_PREAUTH": 0x400000, 32 | "PASSWORD_EXPIRED": 0x800000, 33 | "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, 34 | "PARTIAL_SECRETS_ACCOUNT": 0x04000000 35 | } 36 | 37 | # https://github.com/fox-it/BloodHound.py/blob/6b83660d3b5adedc24e5b2c2d142c524e320ad1c/bloodhound/ad/utils.py#L101 38 | # I really didn't want to just copy paste this but this is the best way I can think of doing it so props to @dirkjanm 39 | functional_level = { 40 | 0: "2000 Mixed/Native", 41 | 1: "2003 Interim", 42 | 2: "2003", 43 | 3: "2008", 44 | 4: "2008 R2", 45 | 5: "2012", 46 | 6: "2012 R2", 47 | 7: "2016" 48 | } 49 | 50 | # https://github.com/dirkjanm/ldapdomaindump/blob/ab1b4c38468509bb43b8943839e987c6680f1b5c/ldapdomaindump/__init__.py#L76 51 | trust_flags = { 52 | "NON_TRANSITIVE":0x00000001, 53 | "UPLEVEL_ONLY":0x00000002, 54 | "QUARANTINED_DOMAIN":0x00000004, 55 | "FOREST_TRANSITIVE":0x00000008, 56 | "CROSS_ORGANIZATION":0x00000010, 57 | "WITHIN_FOREST":0x00000020, 58 | "TREAT_AS_EXTERNAL":0x00000040, 59 | "USES_RC4_ENCRYPTION":0x00000080, 60 | "CROSS_ORGANIZATION_NO_TGT_DELEGATION":0x00000200, 61 | "CROSS_ORGANIZATION_ENABLE_TGT_DELEGATION":0x00000800, 62 | "PIM_TRUST":0x00000400 63 | } 64 | 65 | def ret_os_path(): 66 | if ((sys.platform == 'win32') and (os.environ.get('OS','') == 'Windows_NT')): 67 | return "\\" 68 | else: 69 | return "/" 70 | 71 | def python_to_json(j): 72 | return j.replace("True", "true").replace("False", "false").replace("None", "null") 73 | 74 | class User: 75 | 76 | def __init__(self): 77 | self.AllowedToDelegate = [] 78 | self.ObjectIdentifier = "" 79 | self.PrimaryGroupSid = "" 80 | self.properties = { 81 | "name": None, 82 | "domain": None, 83 | "objectid": None, 84 | "distinguishedname": None, 85 | "highvalue": None, 86 | "unconstraineddelegation": None, 87 | "passwordnotreqd": None, 88 | "enabled": None, 89 | "lastlogon": None, 90 | "lastlogontimestamp": None, 91 | "pwdlastset": None, 92 | "dontreqpreauth": None, 93 | "pwdneverexpires": None, 94 | "sensitive": None, 95 | "serviceprincipalnames": [], 96 | "hasspn": None, 97 | "displayname": None, 98 | "email": None, 99 | "title": None, 100 | "homedirectory": None, 101 | "description": None, 102 | "userpassword": None, 103 | "admincount": None, 104 | "sidhistory": [] 105 | } 106 | self.Aces = [] 107 | self.SPNTargets = [] 108 | self.HasSIDHistory = [] 109 | 110 | def export(self): 111 | buf = '{' + '"AllowedToDelegate": {}, "ObjectIdentifier": "{}", "PrimaryGroupSid": "{}", "Properties": {}, "Aces": {}, "SPNTargets": {}, "HasSIDHistory": {}'.format( 112 | self.AllowedToDelegate, 113 | self.ObjectIdentifier, 114 | self.PrimaryGroupSid, 115 | json.dumps(self.properties, indent=4, separators=(",", ": "), sort_keys=False), 116 | self.Aces, 117 | self.SPNTargets, 118 | self.HasSIDHistory 119 | ) + '}' 120 | return python_to_json(json.loads(json.dumps(buf, indent=4, sort_keys=False, separators=(",", ": ")))) 121 | 122 | class Computer: 123 | 124 | def __init__(self): 125 | self.ObjectIdentifier = "" 126 | self.AllowedToAct = [] 127 | self.PrimaryGroupSid = "" 128 | self.LocalAdmins = [] 129 | self.PSRemoteUsers = [] 130 | self.properties = { 131 | "name": None, 132 | "objectid": None, 133 | "domain": None, 134 | "highvalue": None, 135 | "distinguishedname": None, 136 | "unconstraineddelegation": None, 137 | "enabled": None, 138 | "haslaps": None, 139 | "lastlogontimestamp": None, 140 | "pwdlastset": None, 141 | "serviceprincipalnames": [], 142 | "description": None, 143 | "operatingsystem": None 144 | } 145 | self.RemoteDesktopUsers = [] 146 | self.DcomUsers = [] 147 | self.AllowedToDelegate = [] 148 | self.Sessions = [] 149 | self.Aces = [] 150 | 151 | def export(self): 152 | buf = '{' + '"ObjectIdentifier": "{}", "AllowedToAct": {}, "PrimaryGroupSid": "{}", "LocalAdmins": {}, "PSRemoteUsers": {}, "Properties": {}, "RemoteDesktopUsers": {}, "DcomUsers": {}, "AllowedToDelegate": {}, "Sessions": {}, "Aces": {}'.format( 153 | self.ObjectIdentifier, 154 | self.AllowedToAct, 155 | self.PrimaryGroupSid, 156 | json.dumps(self.LocalAdmins), 157 | self.PSRemoteUsers, 158 | json.dumps(self.properties), 159 | self.RemoteDesktopUsers, 160 | self.DcomUsers, 161 | self.RemoteDesktopUsers, 162 | self.AllowedToDelegate, 163 | self.Sessions, 164 | self.Aces, 165 | ) + '}' 166 | return python_to_json(json.loads(json.dumps(buf, indent=4, sort_keys=False, separators=(",", ": ")))) 167 | 168 | class Group: 169 | 170 | def __init__(self): 171 | self.ObjectIdentifier = None 172 | self.properties = { 173 | "domain": None, 174 | "objectid": None, 175 | "highvalue": None, 176 | "name": None, 177 | "distinguishedname": None, 178 | "admincount": None, 179 | "description": None 180 | } 181 | self.Members = [] 182 | self.Aces = [] 183 | 184 | def export(self): 185 | #self.sanitize() 186 | buf = '{' + '"ObjectIdentifier": "{}", "Properties": {}, "Members": {}, "Aces": {}'.format( 187 | self.ObjectIdentifier, 188 | json.dumps(self.properties), 189 | json.dumps(self.Members), 190 | self.Aces 191 | ) + '}' 192 | return python_to_json(json.loads(json.dumps(buf, indent=4, sort_keys=False, separators=(",", ": ")))) 193 | 194 | class Domain: 195 | 196 | def __init__(self): 197 | self.ObjectIdentifier = None 198 | self.properties = { 199 | "name": None, 200 | "domain": None, 201 | "highvalue": True, 202 | "objectid": None, 203 | "distinguishedname": None, 204 | "description": None, 205 | "functionallevel": None 206 | } 207 | self.Trusts = [] 208 | self.Aces = [] 209 | self.Links = [] 210 | self.Users = [] 211 | self.Computers = [] 212 | self.ChildOus = [] 213 | 214 | def export(self): 215 | buf = '{' + '"ObjectIdentifier": "{}", "Properties": {}, "Trusts": {}, "Aces": {}, "Links": {}, "Users": {}, "Computers": {}, "ChildOus": {}'.format( 216 | self.ObjectIdentifier, 217 | json.dumps(self.properties), 218 | json.dumps(self.Trusts), 219 | self.Aces, 220 | self.Links, 221 | self.Users, 222 | self.Computers, 223 | self.ChildOus 224 | ) + '}' 225 | return python_to_json(json.loads(json.dumps(buf, indent=4, sort_keys=False, separators=(",", ": ")))) 226 | 227 | def check(attr, mask): 228 | if ((attr & mask) > 0): 229 | return True 230 | return False 231 | 232 | def to_epoch(longform): 233 | # 2021-09-30 05:28:09.685524+00:00 234 | try: 235 | utc_time = datetime.strptime(longform, "%Y-%m-%d %H:%M:%S.%f+00:00") 236 | epoch_time = int((utc_time - datetime(1970, 1, 1)).total_seconds()) 237 | return int(epoch_time) 238 | except ValueError: 239 | return -1 240 | 241 | def parse_users(input_folder, output_folder, bh_version): 242 | # https://github.com/dzhibas/SublimePrettyJson/blob/af5a6708d308f60787499e360081bf92afe66156/PrettyJson.py#L48 243 | brace_newline = re.compile(r'^((\s*)".*?":)\s*([{])', re.MULTILINE) 244 | bracket_newline = re.compile(r'^((\s*)".*?":)\s*([\[])', re.MULTILINE) 245 | count = 0 246 | j = json.loads(open(input_folder + ret_os_path() + "domain_users.json", "r").read()) 247 | buf = '{"users": [' 248 | for user in j: 249 | u = User() 250 | u.ObjectIdentifier = user['attributes']['objectSid'][0] 251 | u.PrimaryGroupSid = '-'.join(user['attributes']['objectSid'][0].split("-")[:-1]) + "-" + str(user['attributes']['primaryGroupID'][0]) 252 | 253 | if (('userPrincipalName' in user['attributes'].keys()) and ("/" not in str(user['attributes']['userPrincipalName'][0]))): 254 | u.properties['name'] = str(user['attributes']['userPrincipalName'][0]).upper() 255 | else: 256 | u.properties['name'] = str(user['attributes']['sAMAccountName'][0]).upper() + "@" + '.'.join(str(user['attributes']['distinguishedName'][0]).split(",DC=")[1:]).upper() 257 | 258 | if 'userPrincipalName' in user['attributes'].keys(): 259 | if "@" in str(user['attributes']['userPrincipalName'][0]): 260 | u.properties['domain'] = str(user['attributes']['userPrincipalName'][0]).upper().split("@")[1] 261 | else: 262 | u.properties['domain'] = str(user['attributes']['userPrincipalName'][0]).upper() 263 | else: 264 | u.properties['domain'] = str(u.properties["name"]).upper().split("@")[1] 265 | 266 | u.properties['objectid'] = user['attributes']['objectSid'][0] 267 | u.properties['distinguishedname'] = user['attributes']['distinguishedName'][0] 268 | 269 | if ("$" in u.properties['distinguishedname']): 270 | db[u.properties['distinguishedname']] = [u.ObjectIdentifier, "Computer"] 271 | else: 272 | db[u.properties['distinguishedname']] = [u.ObjectIdentifier, "User"] 273 | 274 | u.properties['highvalue'] = False 275 | for h in hvt: 276 | if (h in str(user['attributes']['primaryGroupID'][0])): 277 | u.properties['highvalue'] = True 278 | 279 | 280 | u.properties['unconstraineddelegation'] = False 281 | if check(user['attributes']['userAccountControl'][0], user_access_control['TRUSTED_FOR_DELEGATION']): 282 | u.properties['unconstraineddelegation'] = True 283 | 284 | # PASSWD_NOTREQD = 0x0020 285 | u.properties["passwordnotreqd"] = False 286 | if check(user['attributes']['userAccountControl'][0], user_access_control['PASSWD_NOTREQD']): 287 | u.properties["passwordnotreqd"] = True 288 | 289 | # ACCOUNTDISABLE = 0x0002 290 | u.properties["enabled"] = False 291 | if (not check(user['attributes']['userAccountControl'][0], user_access_control['ACCOUNTDISABLE'])): 292 | u.properties['enabled'] = True 293 | 294 | if 'lastLogon' in user['attributes'].keys(): 295 | u.properties['lastlogon'] = to_epoch(user['attributes']['lastLogon'][0]) 296 | else: 297 | u.properties['lastlogon'] = -1 298 | 299 | if 'lastLogonTimestamp' in user['attributes'].keys(): 300 | u.properties['lastlogontimestamp'] = to_epoch(user['attributes']['lastLogonTimestamp'][0]) 301 | else: 302 | u.properties['lastlogontimestamp'] = -1 303 | 304 | if 'pwdLastSet' in user['attributes'].keys(): 305 | u.properties['pwdlastset'] = to_epoch(user['attributes']['pwdLastSet'][0]) 306 | else: 307 | u.properties['pwdlastset'] = -1 308 | 309 | u.properties['dontreqpreauth'] = False 310 | if check(user['attributes']['userAccountControl'][0], user_access_control['DONT_REQ_PREAUTH']): 311 | u.properties["dontreqpreauth"] = True 312 | 313 | u.properties['pwdneverexpires'] = False 314 | if check(user['attributes']['userAccountControl'][0], user_access_control['DONT_EXPIRE_PASSWORD']): 315 | u.properties["pwdneverexpires"] = True 316 | 317 | u.properties['sensitive'] = False 318 | u.properties['serviceprincipalnames'] = [] 319 | 320 | if 'servicePrincipalName' in user['attributes'].keys(): 321 | u.properties['hasspn'] = True 322 | for spn in user['attributes']['servicePrincipalName']: 323 | u.properties['serviceprincipalnames'].append(spn) 324 | else: 325 | u.properties['hasspn'] = False 326 | 327 | 328 | if 'displayName' in user['attributes'].keys(): 329 | u.properties['displayname'] = user['attributes']['displayName'][0] 330 | else: 331 | u.properties['displayname'] = user['attributes']['sAMAccountName'][0] 332 | 333 | u.properties['email'] = None 334 | u.properties['title'] = None 335 | u.properties['homedirectory'] = None 336 | 337 | if 'description' in user['attributes'].keys(): 338 | u.properties['description'] = user['attributes']['description'][0] 339 | else: 340 | u.properties['description'] = None 341 | 342 | u.properties['userpassword'] = None 343 | 344 | if 'adminCount' in user['attributes'].keys(): 345 | u.properties['admincount'] = True 346 | else: 347 | u.properties['admincount'] = False 348 | 349 | u.properties['sidhistory'] = [] 350 | 351 | u.Aces = [] 352 | u.SPNTargets = [] 353 | u.HasSIDHistory = [] 354 | 355 | buf += u.export() + ', ' 356 | count += 1 357 | 358 | with open(output_folder + ret_os_path() + "users.json", "w") as outfile: 359 | buf = bracket_newline.sub(r"\1\n\2\3", bracket_newline.sub(r"\1\n\2\3", json.dumps(json.loads(buf[:-2] + '],' + ' "meta": ' + '{' + '"type": "users", "count": {}, "version": {}'.format(count, bh_version) + '}}'), indent=4, sort_keys=False, separators=(",", ": ")))) 360 | outfile.write(buf) 361 | buf = "" 362 | 363 | def build_la_dict(domain_sid, group_sid, member_type): 364 | return { "MemberId" : domain_sid + '-' + group_sid, "MemberType": member_type } 365 | 366 | def parse_computers(input_folder, output_folder, bh_version): 367 | # https://github.com/dzhibas/SublimePrettyJson/blob/af5a6708d308f60787499e360081bf92afe66156/PrettyJson.py#L48 368 | brace_newline = re.compile(r'^((\s*)".*?":)\s*([{])', re.MULTILINE) 369 | bracket_newline = re.compile(r'^((\s*)".*?":)\s*([\[])', re.MULTILINE) 370 | count = 0 371 | j = json.loads(open(input_folder + ret_os_path() + "domain_computers.json", "r").read()) 372 | buf = '{"computers": [' 373 | for comp in j: 374 | c = Computer() 375 | c.ObjectIdentifier = comp['attributes']['objectSid'][0] 376 | c.AllowedToAct = [] 377 | c.PrimaryGroupSid = '-'.join(comp['attributes']['objectSid'][0].split("-")[:-1]) + "-" + str(comp['attributes']['primaryGroupID'][0]) 378 | 379 | sid = '-'.join(comp['attributes']['objectSid'][0].split("-")[:-1]) 380 | c.LocalAdmins = [] 381 | c.LocalAdmins.append(build_la_dict(sid, "519", "Group")) 382 | c.LocalAdmins.append(build_la_dict(sid, "512", "Group")) 383 | c.LocalAdmins.append(build_la_dict(sid, "500", "User")) 384 | 385 | c.PSRemoteUsers = [] 386 | 387 | if 'dNSHostName' in comp['attributes'].keys(): 388 | c.properties["name"] = str(comp['attributes']['dNSHostName'][0]).upper() 389 | else: 390 | c.properties["name"] = str(comp['attributes']['distinguishedName'][0]).split(",CN=")[0].split("=")[1].replace(",OU", "") + "." + '.'.join(str(comp['attributes']['distinguishedName'][0]).split(",DC=")[1:]).upper() 391 | 392 | if 'userPrincipalName' in comp['attributes'].keys(): 393 | c.properties["domain"] = str(comp['attributes']['userPrincipalName'][0]).upper().split(".")[1:] 394 | elif ("." in str(c.properties["name"])): 395 | c.properties["domain"] = '.'.join(str(c.properties["name"]).upper().split(".")[1:]) 396 | else: 397 | # need to manually build domain based off object 398 | c.properties["domain"] = '.'.join(str(comp['attributes']['distinguishedName'][0]).split(",DC=")[1:]).upper() 399 | 400 | c.properties["objectid"] = comp['attributes']['objectSid'][0] 401 | 402 | c.properties["distinguishedname"] = comp['attributes']['distinguishedName'][0] 403 | 404 | c.properties["highvalue"] = False 405 | for h in hvt: 406 | if (h in str(comp['attributes']['primaryGroupID'][0])): 407 | c.properties["highvalue"] = True 408 | 409 | if 'userAccountControl' in comp['attributes'].keys(): 410 | if check(comp['attributes']['userAccountControl'][0], user_access_control['TRUSTED_FOR_DELEGATION']): 411 | c.properties['unconstraineddelegation'] = True 412 | else: 413 | c.properties['unconstraineddelegation'] = False 414 | 415 | 416 | c.properties["enabled"] = False 417 | if (not check(comp['attributes']['userAccountControl'][0], user_access_control['ACCOUNTDISABLE'])): 418 | c.properties['enabled'] = True 419 | 420 | c.properties['haslaps'] = False # TDODO 421 | 422 | if 'lastLogonTimestamp' in comp['attributes'].keys(): 423 | c.properties['lastlogontimestamp'] = to_epoch(comp['attributes']['lastLogonTimestamp'][0]) 424 | else: 425 | c.properties['lastlogontimestamp'] = -1 426 | 427 | if 'pwdLastSet' in comp['attributes'].keys(): 428 | c.properties['pwdlastset'] = to_epoch(comp['attributes']['pwdLastSet'][0]) 429 | else: 430 | c.properties['pwdlastset'] = -1 431 | 432 | if 'servicePrincipalName' in comp['attributes'].keys(): 433 | c.properties['serviceprincipalnames'] = comp['attributes']['servicePrincipalName'] 434 | else: 435 | c.properties['serviceprincipalnames'] = None 436 | 437 | if 'description' in comp['attributes'].keys(): 438 | c.properties['description'] = comp['attributes']['description'][0] 439 | else: 440 | c.properties['description'] = None 441 | 442 | if 'operatingSystem' in comp['attributes'].keys(): 443 | c.properties['operatingsystem'] = comp['attributes']['operatingSystem'] 444 | else: 445 | c.properties['operatingsystem'] = None 446 | 447 | buf += c.export() + ', ' 448 | count += 1 449 | 450 | with open(output_folder + ret_os_path() + "computers.json", "w") as outfile: 451 | buf = bracket_newline.sub(r"\1\n\2\3", bracket_newline.sub(r"\1\n\2\3", json.dumps(json.loads(buf[:-2] + '],' + ' "meta": ' + '{' + '"type": "computers", "count": {}, "version": {}'.format(count, bh_version) + '}}'), indent=4, sort_keys=False, separators=(",", ": ")))) 452 | outfile.write(buf) 453 | buf = "" 454 | 455 | def build_mem_dict(sid, member_type): 456 | return { "MemberId" : sid, "MemberType": member_type } 457 | 458 | def parse_groups(input_folder, output_folder, no_users, bh_version): 459 | # https://github.com/dzhibas/SublimePrettyJson/blob/af5a6708d308f60787499e360081bf92afe66156/PrettyJson.py#L48 460 | brace_newline = re.compile(r'^((\s*)".*?":)\s*([{])', re.MULTILINE) 461 | bracket_newline = re.compile(r'^((\s*)".*?":)\s*([\[])', re.MULTILINE) 462 | count = 0 463 | 464 | if (no_users): 465 | j = json.loads(open(input_folder + ret_os_path() + "domain_users.json", "r").read()) 466 | for user in j: 467 | u = user['attributes']['distinguishedName'][0] 468 | if ("$" in u): 469 | db[u] = [user['attributes']['objectSid'][0], "Computer"] 470 | else: 471 | db[u] = [user['attributes']['objectSid'][0], "User"] 472 | 473 | j = json.loads(open(input_folder + ret_os_path() + "domain_groups.json", "r").read()) 474 | 475 | # fist build up group sids 476 | for group in j: 477 | db[group['attributes']['distinguishedName'][0]] = [group['attributes']['objectSid'][0], "Group"] 478 | 479 | buf = '{"groups": [' 480 | # now build up the whole file 481 | for group in j: 482 | g = Group() 483 | g.ObjectIdentifier = group['attributes']['objectSid'][0] 484 | 485 | if 'userPrincipalName' in group['attributes'].keys(): 486 | g.properties['name'] = str(group['attributes']['userPrincipalName'][0]).upper() 487 | else: 488 | g.properties['name'] = str(group['attributes']['distinguishedName'][0]).split(",CN=")[0].split("=")[1].replace(",OU", "").upper() + "@" + '.'.join(str(group['attributes']['distinguishedName'][0]).split(",DC=")[1:]).upper() 489 | 490 | if 'userPrincipalName' in group['attributes'].keys(): 491 | g.properties['domain'] = str(group['attributes']['userPrincipalName'][0]).upper().split("@")[1] 492 | else: 493 | g.properties['domain'] = str(g.properties["name"]).upper().split("@")[1] 494 | 495 | g.properties['objectid'] = group['attributes']['objectSid'][0] 496 | 497 | g.properties['highvalue'] = False 498 | for h in hvt: 499 | if (h in str(group['attributes']['objectSid'][0]).split("-")[-1:]): 500 | g.properties['highvalue'] = True 501 | 502 | g.properties['distinguishedname'] = group['attributes']['distinguishedName'][0] 503 | 504 | if 'adminCount' in group['attributes'].keys(): 505 | g.properties['admincount'] = True 506 | else: 507 | g.properties['admincount'] = False 508 | 509 | if 'description' in group['attributes'].keys(): 510 | g.properties['description'] = group['attributes']['description'][0] 511 | else: 512 | g.properties['description'] = None 513 | 514 | try: 515 | for m in group['attributes']['member']: 516 | t = db[m] 517 | g.Members.append(build_mem_dict(t[0], t[1])) 518 | except: 519 | pass 520 | 521 | buf += g.export() + ', ' 522 | count += 1 523 | 524 | with open(output_folder + ret_os_path() + "groups.json", "w") as outfile: 525 | buf = bracket_newline.sub(r"\1\n\2\3", bracket_newline.sub(r"\1\n\2\3", json.dumps(json.loads(buf[:-2] + '],' + ' "meta": ' + '{' + '"type": "groups", "count": {}, "version": {}'.format(count, bh_version) + '}}'), indent=4, sort_keys=False, separators=(",", ": ")))) 526 | outfile.write(buf) 527 | buf = "" 528 | 529 | # https://stackoverflow.com/questions/33188413/python-code-to-convert-from-objectsid-to-sid-representation 530 | def sid_to_str(sid): 531 | try: 532 | # Python 3 533 | if str is not bytes: 534 | # revision 535 | revision = int(sid[0]) 536 | # count of sub authorities 537 | sub_authorities = int(sid[1]) 538 | # big endian 539 | identifier_authority = int.from_bytes(sid[2:8], byteorder='big') 540 | # If true then it is represented in hex 541 | if identifier_authority >= 2 ** 32: 542 | identifier_authority = hex(identifier_authority) 543 | 544 | # loop over the count of small endians 545 | sub_authority = '-' + '-'.join([str(int.from_bytes(sid[8 + (i * 4): 12 + (i * 4)], byteorder='little')) for i in range(sub_authorities)]) 546 | # Python 2 547 | else: 548 | revision = int(b2a_hex(sid[0])) 549 | sub_authorities = int(b2a_hex(sid[1])) 550 | identifier_authority = int(b2a_hex(sid[2:8]), 16) 551 | if identifier_authority >= 2 ** 32: 552 | identifier_authority = hex(identifier_authority) 553 | 554 | sub_authority = '-' + '-'.join([str(int(b2a_hex(sid[11 + (i * 4): 7 + (i * 4): -1]), 16)) for i in range(sub_authorities)]) 555 | objectSid = 'S-' + str(revision) + '-' + str(identifier_authority) + sub_authority 556 | 557 | return objectSid 558 | except Exception: 559 | pass 560 | 561 | def parse_domains(input_folder, output_folder, bh_version): 562 | # https://github.com/dzhibas/SublimePrettyJson/blob/af5a6708d308f60787499e360081bf92afe66156/PrettyJson.py#L48 563 | brace_newline = re.compile(r'^((\s*)".*?":)\s*([{])', re.MULTILINE) 564 | bracket_newline = re.compile(r'^((\s*)".*?":)\s*([\[])', re.MULTILINE) 565 | 566 | count = 0 567 | sid = None 568 | j = json.loads(open(input_folder + ret_os_path() + "domain_policy.json", "r").read()) 569 | buf = '{"domains": [' 570 | for dom in j: 571 | d = Domain() 572 | if 'objectSid' in dom['attributes'].keys(): 573 | d.ObjectIdentifier = dom['attributes']['objectSid'][0] 574 | d.properties['objectid'] = dom['attributes']['objectSid'][0] 575 | else: 576 | d.ObjectIdentifier = None 577 | d.properties['objectid'] = None 578 | 579 | # if 'name' in dom['attributes'].keys(): 580 | # d.properties['name'] = dom['attributes']['name'][0].upper() 581 | # else: 582 | # d.properties['name'] = None 583 | 584 | if 'cn' in dom['attributes'].keys(): 585 | d.properties['domain'] = dom['attributes']['cn'][0].upper() 586 | elif 'distinguishedName' in dom['attributes'].keys(): 587 | d.properties['domain'] = dom['attributes']['distinguishedName'][0].upper().replace(",DC=", ".").replace("DC=", "") 588 | else: 589 | d.properties['domain'] = dom['attributes']['cn'][0].upper() 590 | 591 | d.properties['name'] = d.properties['domain'] 592 | 593 | if 'distinguishedName' in dom['attributes'].keys(): 594 | d.properties['distinguishedname'] = dom['attributes']['distinguishedName'][0].upper() 595 | elif 'dn' in dom.keys(): 596 | d.properties['distinguishedname'] = dom['dn'].upper() 597 | else: 598 | d.properties['distinguisedname'] = None 599 | 600 | if 'description' in dom['attributes'].keys(): 601 | d.properties['description'] = dom['attributes']['description'][0] 602 | else: 603 | d.properties['description'] = None 604 | 605 | if 'msDS-Behavior-Version' in dom['attributes'].keys(): 606 | d.properties['functionallevel'] = functional_level[int(dom['attributes']['msDS-Behavior-Version'][0])] 607 | else: 608 | d.properties['functionallevel'] = None 609 | buf += d.export() + ', ' 610 | count += 1 611 | 612 | with open(output_folder + ret_os_path() + "domains.json", "w") as outfile: 613 | buf = bracket_newline.sub(r"\1\n\2\3", bracket_newline.sub(r"\1\n\2\3", json.dumps(json.loads(buf[:-2] + '],' + ' "meta": ' + '{' + '"type": "domains", "count": {}, "version": {}'.format(count, bh_version) + '}}'), indent=4, sort_keys=False, separators=(",", ": ")))) 614 | outfile.write(buf) 615 | buf = "" 616 | 617 | 618 | def parse_domain_trusts(input_folder, output_folder, bh_version): 619 | count = 0 620 | sid = None 621 | j = json.loads(open(input_folder + ret_os_path() + "domain_trusts.json", "r").read()) 622 | buf = '{"domains": [' 623 | for dom in j: 624 | d = Domain() 625 | if ("base64".upper() in dom['attributes']['securityIdentifier'][0]['encoding'].upper()): 626 | sid = sid_to_str(base64.b64decode(dom['attributes']['securityIdentifier'][0]['encoded'])) 627 | d.ObjectIdentifier = sid 628 | else: 629 | d.ObjectIdentifier = None 630 | d.properties['name'] = dom['attributes']['name'][0].upper() 631 | d.properties['domain'] = dom['attributes']['cn'][0].upper() 632 | d.properties['objectid'] = sid 633 | d.properties['distinguishedname'] = dom['attributes']['distinguishedName'][0].upper() 634 | 635 | if 'description' in dom['attributes'].keys(): 636 | d.properties['description'] = dom['attributes']['description'][0] 637 | else: 638 | d.properties['description'] = None 639 | 640 | if 'msDS-Behavior-Version' in dom['attributes'].keys(): 641 | d.properties['functionallevel'] = functional_level[int(dom['attributes']['msDS-Behavior-Version'][0])] 642 | else: 643 | d.properties['functionallevel'] = None 644 | 645 | target_domain_sid = None 646 | already_found = json.loads(open(output_folder + ret_os_path() + "domains.json", "r").read()) 647 | for dom2 in already_found['domains']: 648 | if (dom['attributes']['trustPartner'][0].upper() == dom2['Properties']['domain']): 649 | target_domain_sid = dom2['ObjectIdentifier'] 650 | 651 | sid_filtering = None 652 | if (dom['attributes']['trustAttributes'][0] & trust_flags['QUARANTINED_DOMAIN']): 653 | sid_filtering = True 654 | else: 655 | sid_filtering = False 656 | 657 | transitive = False 658 | if (dom['attributes']['trustAttributes'][0] & trust_flags['FOREST_TRANSITIVE']): 659 | transitive = True 660 | sid_filtering = True 661 | 662 | if (target_domain_sid): 663 | d.Trusts.append({ 664 | "TargetDomain": dom['attributes']['trustPartner'][0].upper(), 665 | "TargetDomainSid": target_domain_sid, 666 | "IsTransitive": transitive, 667 | "TrustDirection": int(dom['attributes']['trustDirection'][0]), 668 | "TrustType": int(dom['attributes']['trustType'][0]), 669 | "SidFilteringEnabled": sid_filtering 670 | }) 671 | 672 | buf += d.export() + ', ' 673 | count += 1 674 | 675 | j = json.loads(open(output_folder + ret_os_path() + "domains.json", "r").read()) 676 | 677 | if (count > 0): 678 | new_domains = json.loads(buf[:-2] + "]}") 679 | for dom in new_domains['domains']: 680 | j['domains'].append(dom) 681 | j['meta']['count'] += count 682 | with open(output_folder + ret_os_path() + "domains.json", "w") as outfile: 683 | outfile.write(json.dumps(j)) 684 | else: 685 | # we have no domain trusts, stop doing anything 686 | return 687 | 688 | if __name__ == '__main__': 689 | parser = argparse.ArgumentParser( 690 | formatter_class=argparse.RawDescriptionHelpFormatter, 691 | description='Convert ldapdomaindump to Bloodhound', 692 | epilog=textwrap.dedent('''Examples:\npython3 ldd2bh.py -i ldd -o bh''') 693 | ) 694 | 695 | parser.add_argument('-i','--input', dest="input_folder", default=".", required=False, help='Input Directory for ldapdomaindump data, default: current directory') 696 | parser.add_argument('-o','--output', dest="output_folder", default=".", required=False, help='Output Directory for Bloodhound data, default: current directory') 697 | parser.add_argument('-a','--all', action='store_true', default=True, required=False, help='Output only users, default: True') 698 | parser.add_argument('-u','--users', action='store_true', default=False, required=False, help='Output only users, default: False') 699 | parser.add_argument('-c','--computers', action='store_true', default=False, required=False, help='Output only computers, default: False') 700 | parser.add_argument('-g','--groups', action='store_true', default=False, required=False, help='Output only groups, default: False') 701 | parser.add_argument('-d','--domains', action='store_true', default=False, required=False, help='Output only domains, default: False') 702 | parser.add_argument('-b','--bh-version', dest='bh_version', default=3, type=int, required=False, help='Bloodhound data format version (only 3 for now), default: 3') 703 | 704 | args = parser.parse_args() 705 | 706 | if ((args.bh_version != 3)): 707 | raise argparse.ArgumentTypeError('Invalid Bloodhound file version given! New version support might come in the future.') 708 | 709 | if ((args.input_folder != ".") and (args.output_folder != ".")): 710 | if (sum([args.users, args.computers, args.groups, args.domains]) == 0): 711 | args.users = True 712 | args.computers = True 713 | args.groups = True 714 | args.domains = True 715 | if (args.users): 716 | print("Parsing users...") 717 | parse_users(args.input_folder, args.output_folder, args.bh_version) 718 | if (args.computers): 719 | print("Parsing computers...") 720 | parse_computers(args.input_folder, args.output_folder, args.bh_version) 721 | if (args.groups): 722 | print("Parsing groups...") 723 | parse_groups(args.input_folder, args.output_folder, not args.users, args.bh_version) 724 | if (args.domains): 725 | print("Parsing domains...") 726 | parse_domains(args.input_folder, args.output_folder, args.bh_version) 727 | parse_domain_trusts(args.input_folder, args.output_folder, args.bh_version) 728 | print("Done!") 729 | else: 730 | parser.print_help() 731 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | argparse 2 | --------------------------------------------------------------------------------