1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793
|
#!/usr/bin/env python3
# Impacket - Collection of Python classes for working with network protocols.
#
# Copyright Fortra, LLC and its affiliated companies
#
# All rights reserved.
#
# This software is provided under a slightly modified version
# of the Apache Software License. See the accompanying LICENSE file
# for more information.
#
# Description:
# Python script to read and manage the Discretionary Access Control List of an object
#
# Authors:
# Charlie BROMBERG (@_nwodtuhs)
# Guillaume DAUMAS (@BlWasp_)
# Lucien DOUSTALY (@Wlayzz)
#
import argparse
import binascii
import codecs
import json
import logging
import os
import sys
import traceback
import datetime
import ldap3
import ldapdomaindump
from enum import Enum
from ldap3.protocol.formatters.formatters import format_sid
from impacket import version
from impacket.examples import logger, utils
from impacket.ldap import ldaptypes
from impacket.msada_guids import SCHEMA_OBJECTS, EXTENDED_RIGHTS
from ldap3.utils.conv import escape_filter_chars
from ldap3.protocol.microsoft import security_descriptor_control
from impacket.uuid import string_to_bin, bin_to_string
from impacket.examples.utils import init_ldap_session, parse_identity
OBJECT_TYPES_GUID = {}
OBJECT_TYPES_GUID.update(SCHEMA_OBJECTS)
OBJECT_TYPES_GUID.update(EXTENDED_RIGHTS)
# Universal SIDs
WELL_KNOWN_SIDS = {
'S-1-0': 'Null Authority',
'S-1-0-0': 'Nobody',
'S-1-1': 'World Authority',
'S-1-1-0': 'Everyone',
'S-1-2': 'Local Authority',
'S-1-2-0': 'Local',
'S-1-2-1': 'Console Logon',
'S-1-3': 'Creator Authority',
'S-1-3-0': 'Creator Owner',
'S-1-3-1': 'Creator Group',
'S-1-3-2': 'Creator Owner Server',
'S-1-3-3': 'Creator Group Server',
'S-1-3-4': 'Owner Rights',
'S-1-5-80-0': 'All Services',
'S-1-4': 'Non-unique Authority',
'S-1-5': 'NT Authority',
'S-1-5-1': 'Dialup',
'S-1-5-2': 'Network',
'S-1-5-3': 'Batch',
'S-1-5-4': 'Interactive',
'S-1-5-6': 'Service',
'S-1-5-7': 'Anonymous',
'S-1-5-8': 'Proxy',
'S-1-5-9': 'Enterprise Domain Controllers',
'S-1-5-10': 'Principal Self',
'S-1-5-11': 'Authenticated Users',
'S-1-5-12': 'Restricted Code',
'S-1-5-13': 'Terminal Server Users',
'S-1-5-14': 'Remote Interactive Logon',
'S-1-5-15': 'This Organization',
'S-1-5-17': 'This Organization',
'S-1-5-18': 'Local System',
'S-1-5-19': 'NT Authority',
'S-1-5-20': 'NT Authority',
'S-1-5-32-544': 'Administrators',
'S-1-5-32-545': 'Users',
'S-1-5-32-546': 'Guests',
'S-1-5-32-547': 'Power Users',
'S-1-5-32-548': 'Account Operators',
'S-1-5-32-549': 'Server Operators',
'S-1-5-32-550': 'Print Operators',
'S-1-5-32-551': 'Backup Operators',
'S-1-5-32-552': 'Replicators',
'S-1-5-64-10': 'NTLM Authentication',
'S-1-5-64-14': 'SChannel Authentication',
'S-1-5-64-21': 'Digest Authority',
'S-1-5-80': 'NT Service',
'S-1-5-83-0': 'NT VIRTUAL MACHINE\\Virtual Machines',
'S-1-16-0': 'Untrusted Mandatory Level',
'S-1-16-4096': 'Low Mandatory Level',
'S-1-16-8192': 'Medium Mandatory Level',
'S-1-16-8448': 'Medium Plus Mandatory Level',
'S-1-16-12288': 'High Mandatory Level',
'S-1-16-16384': 'System Mandatory Level',
'S-1-16-20480': 'Protected Process Mandatory Level',
'S-1-16-28672': 'Secure Process Mandatory Level',
'S-1-5-32-554': 'BUILTIN\\Pre-Windows 2000 Compatible Access',
'S-1-5-32-555': 'BUILTIN\\Remote Desktop Users',
'S-1-5-32-557': 'BUILTIN\\Incoming Forest Trust Builders',
'S-1-5-32-556': 'BUILTIN\\Network Configuration Operators',
'S-1-5-32-558': 'BUILTIN\\Performance Monitor Users',
'S-1-5-32-559': 'BUILTIN\\Performance Log Users',
'S-1-5-32-560': 'BUILTIN\\Windows Authorization Access Group',
'S-1-5-32-561': 'BUILTIN\\Terminal Server License Servers',
'S-1-5-32-562': 'BUILTIN\\Distributed COM Users',
'S-1-5-32-569': 'BUILTIN\\Cryptographic Operators',
'S-1-5-32-573': 'BUILTIN\\Event Log Readers',
'S-1-5-32-574': 'BUILTIN\\Certificate Service DCOM Access',
'S-1-5-32-575': 'BUILTIN\\RDS Remote Access Servers',
'S-1-5-32-576': 'BUILTIN\\RDS Endpoint Servers',
'S-1-5-32-577': 'BUILTIN\\RDS Management Servers',
'S-1-5-32-578': 'BUILTIN\\Hyper-V Administrators',
'S-1-5-32-579': 'BUILTIN\\Access Control Assistance Operators',
'S-1-5-32-580': 'BUILTIN\\Remote Management Users',
}
# GUID rights enum
# GUID thats permits to identify extended rights in an ACE
# https://docs.microsoft.com/en-us/windows/win32/adschema/a-rightsguid
class RIGHTS_GUID(Enum):
WriteMembers = "bf9679c0-0de6-11d0-a285-00aa003049e2"
ResetPassword = "00299570-246d-11d0-a768-00aa006e0529"
DS_Replication_Get_Changes = "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2"
DS_Replication_Get_Changes_All = "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2"
# ACE flags enum
# New ACE at the end of SACL for inheritance and access return system-audit
# https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-addauditaccessobjectace
class ACE_FLAGS(Enum):
CONTAINER_INHERIT_ACE = ldaptypes.ACE.CONTAINER_INHERIT_ACE
FAILED_ACCESS_ACE_FLAG = ldaptypes.ACE.FAILED_ACCESS_ACE_FLAG
INHERIT_ONLY_ACE = ldaptypes.ACE.INHERIT_ONLY_ACE
INHERITED_ACE = ldaptypes.ACE.INHERITED_ACE
NO_PROPAGATE_INHERIT_ACE = ldaptypes.ACE.NO_PROPAGATE_INHERIT_ACE
OBJECT_INHERIT_ACE = ldaptypes.ACE.OBJECT_INHERIT_ACE
SUCCESSFUL_ACCESS_ACE_FLAG = ldaptypes.ACE.SUCCESSFUL_ACCESS_ACE_FLAG
# ACE flags enum
# For an ACE, flags that indicate if the ObjectType and the InheritedObjecType are set with a GUID
# Since these two flags are the same for Allowed and Denied access, the same class will be used from 'ldaptypes'
# https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-access_allowed_object_ace
class OBJECT_ACE_FLAGS(Enum):
ACE_OBJECT_TYPE_PRESENT = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT
ACE_INHERITED_OBJECT_TYPE_PRESENT = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_INHERITED_OBJECT_TYPE_PRESENT
# Access Mask enum
# Access mask permits to encode principal's rights to an object. This is the rights the principal behind the specified SID has
# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/7a53f60e-e730-4dfe-bbe9-b21b62eb790b
# https://docs.microsoft.com/en-us/windows/win32/api/iads/ne-iads-ads_rights_enum?redirectedfrom=MSDN
class ACCESS_MASK(Enum):
# Generic Rights
GenericRead = 0x80000000 # ADS_RIGHT_GENERIC_READ
GenericWrite = 0x40000000 # ADS_RIGHT_GENERIC_WRITE
GenericExecute = 0x20000000 # ADS_RIGHT_GENERIC_EXECUTE
GenericAll = 0x10000000 # ADS_RIGHT_GENERIC_ALL
# Maximum Allowed access type
MaximumAllowed = 0x02000000
# Access System Acl access type
AccessSystemSecurity = 0x01000000 # ADS_RIGHT_ACCESS_SYSTEM_SECURITY
# Standard access types
Synchronize = 0x00100000 # ADS_RIGHT_SYNCHRONIZE
WriteOwner = 0x00080000 # ADS_RIGHT_WRITE_OWNER
WriteDACL = 0x00040000 # ADS_RIGHT_WRITE_DAC
ReadControl = 0x00020000 # ADS_RIGHT_READ_CONTROL
Delete = 0x00010000 # ADS_RIGHT_DELETE
# Specific rights
AllExtendedRights = 0x00000100 # ADS_RIGHT_DS_CONTROL_ACCESS
ListObject = 0x00000080 # ADS_RIGHT_DS_LIST_OBJECT
DeleteTree = 0x00000040 # ADS_RIGHT_DS_DELETE_TREE
WriteProperties = 0x00000020 # ADS_RIGHT_DS_WRITE_PROP
ReadProperties = 0x00000010 # ADS_RIGHT_DS_READ_PROP
Self = 0x00000008 # ADS_RIGHT_DS_SELF
ListChildObjects = 0x00000004 # ADS_RIGHT_ACTRL_DS_LIST
DeleteChild = 0x00000002 # ADS_RIGHT_DS_DELETE_CHILD
CreateChild = 0x00000001 # ADS_RIGHT_DS_CREATE_CHILD
# Simple permissions enum
# Simple permissions are combinaisons of extended permissions
# https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc783530(v=ws.10)?redirectedfrom=MSDN
class SIMPLE_PERMISSIONS(Enum):
FullControl = 0xf01ff
Modify = 0x0301bf
ReadAndExecute = 0x0200a9
ReadAndWrite = 0x02019f
Read = 0x20094
Write = 0x200bc
# Mask ObjectType field enum
# Possible values for the Mask field in object-specific ACE (permitting to specify extended rights in the ObjectType field for example)
# Since these flags are the same for Allowed and Denied access, the same class will be used from 'ldaptypes'
# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/c79a383c-2b3f-4655-abe7-dcbb7ce0cfbe
class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum):
ControlAccess = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS
CreateChild = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CREATE_CHILD
DeleteChild = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_DELETE_CHILD
ReadProperty = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_READ_PROP
WriteProperty = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP
Self = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_SELF
class DACLedit(object):
"""docstring for setrbcd"""
def __init__(self, ldap_server, ldap_session, args):
super(DACLedit, self).__init__()
self.ldap_server = ldap_server
self.ldap_session = ldap_session
self.target_sAMAccountName = args.target_sAMAccountName
self.target_SID = args.target_SID
self.target_DN = args.target_DN
self.principal_sAMAccountName = args.principal_sAMAccountName
self.principal_SID = args.principal_SID
self.principal_DN = args.principal_DN
self.ace_type = args.ace_type
self.rights = args.rights
self.rights_guid = args.rights_guid
self.filename = args.filename
self.inheritance = args.inheritance
if self.inheritance:
logging.info("NB: objects with adminCount=1 will no inherit ACEs from their parent container/OU")
logging.debug('Initializing domainDumper()')
cnf = ldapdomaindump.domainDumpConfig()
cnf.basepath = None
self.domain_dumper = ldapdomaindump.domainDumper(self.ldap_server, self.ldap_session, cnf)
if args.mask is not None:
if args.mask.startswith("0x"):
self.force_mask = int(args.mask, 16)
elif args.mask == "readwrite":
self.force_mask = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_READ_PROP + \
ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP
elif args.mask == "write":
self.force_mask = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP
elif args.mask == "self":
self.force_mask = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_SELF
elif args.mask == "allext":
self.force_mask = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS
else:
self.force_mask = None
if self.target_sAMAccountName or self.target_SID or self.target_DN:
# Searching for target account with its security descriptor
self.search_target_principal_security_descriptor()
# Extract security descriptor data
self.principal_raw_security_descriptor = self.target_principal['nTSecurityDescriptor'].raw_values[0]
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
# Searching for the principal SID if any principal argument was given and principal_SID wasn't
if self.principal_SID is None and self.principal_sAMAccountName is not None or self.principal_DN is not None:
_lookedup_principal = ""
if self.principal_sAMAccountName is not None:
_lookedup_principal = self.principal_sAMAccountName
self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(_lookedup_principal), attributes=['objectSid'])
elif self.principal_DN is not None:
_lookedup_principal = self.principal_DN
self.ldap_session.search(_lookedup_principal, '(distinguishedName=%s)' % _lookedup_principal, attributes=['objectSid'])
try:
self.principal_SID = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0])
logging.debug("Found principal SID: %s" % self.principal_SID)
except IndexError:
logging.error('Principal SID not found in LDAP (%s)' % _lookedup_principal)
exit(1)
# Main read funtion
# Prints the parsed DACL
def read(self):
parsed_dacl = self.parseDACL(self.principal_security_descriptor['Dacl'])
self.printparsedDACL(parsed_dacl)
return
# Main write function
# Attempts to add a new ACE to a DACL
def write(self):
# Creates ACEs with the specified GUIDs and the SID, or FullControl if no GUID is specified
# Append the ACEs in the DACL locally
if self.rights == "FullControl" and self.rights_guid is None:
logging.debug("Appending ACE (%s --(FullControl)--> %s)" % (self.principal_SID, format_sid(self.target_SID)))
self.principal_security_descriptor['Dacl'].aces.append(self.create_ace(SIMPLE_PERMISSIONS.FullControl.value, self.principal_SID, self.ace_type))
elif self.rights == "Custom" and self.force_mask is not None:
logging.debug("Appending ACE (%s --(Custom)--> %s)" % (self.principal_SID, format_sid(self.target_SID)))
self.principal_security_descriptor['Dacl'].aces.append(self.create_ace(self.force_mask, self.principal_SID, self.ace_type))
else:
for rights_guid in self.build_guids_for_rights():
logging.debug("Appending ACE (%s --(%s)--> %s)" % (self.principal_SID, rights_guid, format_sid(self.target_SID)))
self.principal_security_descriptor['Dacl'].aces.append(self.create_object_ace(rights_guid, self.principal_SID, self.ace_type, force_mask=self.force_mask))
# Backups current DACL before add the new one
self.backup()
# Effectively push the DACL with the new ACE
self.modify_secDesc_for_dn(self.target_principal.entry_dn, self.principal_security_descriptor)
return
# Attempts to remove an ACE from the DACL
# To do it, a new DACL is built locally with all the ACEs that must NOT BE removed, and this new DACL is pushed on the server
def remove(self):
compare_aces = []
# Creates ACEs with the specified GUIDs and the SID, or FullControl if no GUID is specified
# These ACEs will be used as comparison templates
if self.rights == "FullControl" and self.rights_guid is None:
compare_aces.append(self.create_ace(SIMPLE_PERMISSIONS.FullControl.value, self.principal_SID, self.ace_type))
elif self.rights == "Custom" and self.force_mask is not None:
compare_aces.append(self.create_ace(self.force_mask, self.principal_SID, self.ace_type))
else:
for rights_guid in self.build_guids_for_rights():
compare_aces.append(self.create_object_ace(rights_guid, self.principal_SID, self.ace_type, force_mask=self.force_mask))
new_dacl = []
i = 0
dacl_must_be_replaced = False
for ace in self.principal_security_descriptor['Dacl'].aces:
ace_must_be_removed = False
for compare_ace in compare_aces:
# To be sure the good ACEs are removed, multiple fields are compared between the templates and the ACEs in the DACL
# - ACE type
# - ACE flags
# - Access masks
# - Revision
# - SubAuthorityCount
# - SubAuthority
# - IdentifierAuthority value
if ace['AceType'] == compare_ace['AceType'] \
and ace['AceFlags'] == compare_ace['AceFlags']\
and ace['Ace']['Mask']['Mask'] == compare_ace['Ace']['Mask']['Mask']\
and ace['Ace']['Sid']['Revision'] == compare_ace['Ace']['Sid']['Revision']\
and ace['Ace']['Sid']['SubAuthorityCount'] == compare_ace['Ace']['Sid']['SubAuthorityCount']\
and ace['Ace']['Sid']['SubAuthority'] == compare_ace['Ace']['Sid']['SubAuthority']\
and ace['Ace']['Sid']['IdentifierAuthority']['Value'] == compare_ace['Ace']['Sid']['IdentifierAuthority']['Value']:
# If the ACE has an ObjectType, the GUIDs must match
if 'ObjectType' in ace['Ace'].fields.keys() and 'ObjectType' in compare_ace['Ace'].fields.keys():
if ace['Ace']['ObjectType'] == compare_ace['Ace']['ObjectType']:
ace_must_be_removed = True
dacl_must_be_replaced = True
else:
ace_must_be_removed = True
dacl_must_be_replaced = True
# If the ACE doesn't match any ACEs from the template list, it is added to the DACL that will be pushed
if not ace_must_be_removed:
new_dacl.append(ace)
elif logging.getLogger().level == logging.DEBUG:
logging.debug("This ACE will be removed")
self.printparsedACE(self.parseACE(ace))
i += 1
# If at least one ACE must been removed
if dacl_must_be_replaced:
self.principal_security_descriptor['Dacl'].aces = new_dacl
self.backup()
self.modify_secDesc_for_dn(self.target_principal.entry_dn, self.principal_security_descriptor)
else:
logging.info("Nothing to remove...")
# Permits to backup a DACL before a modification
# This function is called before any writing action (write, remove or restore)
def backup(self):
backup = {}
backup["sd"] = binascii.hexlify(self.principal_raw_security_descriptor).decode('utf-8')
backup["dn"] = self.target_principal.entry_dn
if not self.filename:
self.filename = 'dacledit-%s.bak' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
else:
if os.path.exists(self.filename):
logging.info("File %s already exists, I'm refusing to overwrite it, setting another filename" % self.filename)
self.filename = 'dacledit-%s.bak' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
with codecs.open(self.filename, 'w', 'utf-8') as outfile:
json.dump(backup, outfile)
logging.info('DACL backed up to %s', self.filename)
# Permits to restore a saved DACL
def restore(self):
# Opens and load the file where the DACL has been saved
with codecs.open(self.filename, 'r', 'utf-8') as infile:
restore = json.load(infile)
assert "sd" in restore.keys()
assert "dn" in restore.keys()
# Extracts the Security Descriptor and converts it to the good ldaptypes format
new_raw_security_descriptor = binascii.unhexlify(restore["sd"].encode('utf-8'))
new_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=new_raw_security_descriptor)
self.target_DN = restore["dn"]
# Searching for target account with its security descriptor
self.search_target_principal_security_descriptor()
# Extract security descriptor data
self.principal_raw_security_descriptor = self.target_principal['nTSecurityDescriptor'].raw_values[0]
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
# Do a backup of the actual DACL and push the restoration
self.backup()
logging.info('Restoring DACL')
self.modify_secDesc_for_dn(self.target_DN, new_security_descriptor)
# Attempts to retrieve the DACL in the Security Descriptor of the specified target
def search_target_principal_security_descriptor(self):
_lookedup_principal = ""
# Set SD flags to only query for DACL
controls = security_descriptor_control(sdflags=0x04)
if self.target_sAMAccountName is not None:
_lookedup_principal = self.target_sAMAccountName
self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(_lookedup_principal), attributes=['nTSecurityDescriptor'], controls=controls)
elif self.target_SID is not None:
_lookedup_principal = self.target_SID
self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % _lookedup_principal, attributes=['nTSecurityDescriptor'], controls=controls)
elif self.target_DN is not None:
_lookedup_principal = self.target_DN
self.ldap_session.search(_lookedup_principal, '(distinguishedName=%s)' % _lookedup_principal, attributes=['nTSecurityDescriptor'], controls=controls)
try:
self.target_principal = self.ldap_session.entries[0]
logging.debug('Target principal found in LDAP (%s)' % _lookedup_principal)
except IndexError:
logging.error('Target principal not found in LDAP (%s)' % _lookedup_principal)
exit(0)
# Attempts to retieve the SID and Distinguisehd Name from the sAMAccountName
# Not used for the moment
# - samname : a sAMAccountName
def get_user_info(self, samname):
self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(samname), attributes=['objectSid'])
try:
dn = self.ldap_session.entries[0].entry_dn
sid = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0])
return dn, sid
except IndexError:
logging.error('User not found in LDAP: %s' % samname)
return False
# Attempts to resolve a SID and return the corresponding samaccountname
# - sid : the SID to resolve
def resolveSID(self, sid):
# Tries to resolve the SID from the well known SIDs
if sid in WELL_KNOWN_SIDS.keys():
return WELL_KNOWN_SIDS[sid]
# Tries to resolve the SID from the LDAP domain dump
else:
self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % sid, attributes=['samaccountname'])
try:
dn = self.ldap_session.entries[0].entry_dn
samname = self.ldap_session.entries[0]['samaccountname']
return samname
except IndexError:
logging.debug('SID not found in LDAP: %s' % sid)
return ""
# Parses a full DACL
# - dacl : the DACL to parse, submitted in a Security Desciptor format
def parseDACL(self, dacl):
parsed_dacl = []
logging.info("Parsing DACL")
i = 0
for ace in dacl['Data']:
parsed_ace = self.parseACE(ace)
parsed_dacl.append(parsed_ace)
i += 1
return parsed_dacl
# Parses an access mask to extract the different values from a simple permission
# https://stackoverflow.com/questions/28029872/retrieving-security-descriptor-and-getting-number-for-filesystemrights
# - fsr : the access mask to parse
def parsePerms(self, fsr):
_perms = []
for PERM in SIMPLE_PERMISSIONS:
if (fsr & PERM.value) == PERM.value:
_perms.append(PERM.name)
fsr = fsr & (~ PERM.value)
for PERM in ACCESS_MASK:
if fsr & PERM.value:
_perms.append(PERM.name)
return _perms
# Parses a specified ACE and extract the different values (Flags, Access Mask, Trustee, ObjectType, InheritedObjectType)
# - ace : the ACE to parse
def parseACE(self, ace):
# For the moment, only the Allowed and Denied Access ACE are supported
if ace['TypeName'] in [ "ACCESS_ALLOWED_ACE", "ACCESS_ALLOWED_OBJECT_ACE", "ACCESS_DENIED_ACE", "ACCESS_DENIED_OBJECT_ACE" ]:
parsed_ace = {}
parsed_ace['ACE Type'] = ace['TypeName']
# Retrieves ACE's flags
_ace_flags = []
for FLAG in ACE_FLAGS:
if ace.hasFlag(FLAG.value):
_ace_flags.append(FLAG.name)
parsed_ace['ACE flags'] = ", ".join(_ace_flags) or "None"
# For standard ACE
# Extracts the access mask (by parsing the simple permissions) and the principal's SID
if ace['TypeName'] in [ "ACCESS_ALLOWED_ACE", "ACCESS_DENIED_ACE" ]:
parsed_ace['Access mask'] = "%s (0x%x)" % (", ".join(self.parsePerms(ace['Ace']['Mask']['Mask'])), ace['Ace']['Mask']['Mask'])
parsed_ace['Trustee (SID)'] = "%s (%s)" % (self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or "UNKNOWN", ace['Ace']['Sid'].formatCanonical())
# For object-specific ACE
elif ace['TypeName'] in [ "ACCESS_ALLOWED_OBJECT_ACE", "ACCESS_DENIED_OBJECT_ACE" ]:
# Extracts the mask values. These values will indicate the ObjectType purpose
_access_mask_flags = []
for FLAG in ALLOWED_OBJECT_ACE_MASK_FLAGS:
if ace['Ace']['Mask'].hasPriv(FLAG.value):
_access_mask_flags.append(FLAG.name)
parsed_ace['Access mask'] = "%s (0x%x)" % (", ".join(_access_mask_flags), ace['Ace']['Mask']['Mask'])
# Extracts the ACE flag values and the trusted SID
_object_flags = []
for FLAG in OBJECT_ACE_FLAGS:
if ace['Ace'].hasFlag(FLAG.value):
_object_flags.append(FLAG.name)
parsed_ace['Flags'] = ", ".join(_object_flags) or "None"
# Extracts the ObjectType GUID values
if ace['Ace']['ObjectTypeLen'] != 0:
obj_type = bin_to_string(ace['Ace']['ObjectType']).lower()
try:
parsed_ace['Object type (GUID)'] = "%s (%s)" % (OBJECT_TYPES_GUID[obj_type], obj_type)
except KeyError:
parsed_ace['Object type (GUID)'] = "UNKNOWN (%s)" % obj_type
# Extracts the InheritedObjectType GUID values
if ace['Ace']['InheritedObjectTypeLen'] != 0:
inh_obj_type = bin_to_string(ace['Ace']['InheritedObjectType']).lower()
try:
parsed_ace['Inherited type (GUID)'] = "%s (%s)" % (OBJECT_TYPES_GUID[inh_obj_type], inh_obj_type)
except KeyError:
parsed_ace['Inherited type (GUID)'] = "UNKNOWN (%s)" % inh_obj_type
# Extract the Trustee SID (the object that has the right over the DACL bearer)
parsed_ace['Trustee (SID)'] = "%s (%s)" % (self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or "UNKNOWN", ace['Ace']['Sid'].formatCanonical())
else:
# If the ACE is not an access allowed
logging.debug("ACE Type (%s) unsupported for parsing yet, feel free to contribute" % ace['TypeName'])
parsed_ace = {}
parsed_ace['ACE type'] = ace['TypeName']
_ace_flags = []
for FLAG in ACE_FLAGS:
if ace.hasFlag(FLAG.value):
_ace_flags.append(FLAG.name)
parsed_ace['ACE flags'] = ", ".join(_ace_flags) or "None"
parsed_ace['DEBUG'] = "ACE type not supported for parsing by dacleditor.py, feel free to contribute"
return parsed_ace
# Prints a full DACL by printing each parsed ACE
# - parsed_dacl : a parsed DACL from parseDACL()
def printparsedDACL(self, parsed_dacl):
# Attempts to retrieve the principal's SID if it's a write action
if self.principal_SID is None and self.principal_sAMAccountName or self.principal_DN:
if self.principal_sAMAccountName is not None:
_lookedup_principal = self.principal_sAMAccountName
self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(_lookedup_principal), attributes=['objectSid'])
elif self.principal_DN is not None:
_lookedup_principal = self.principal_DN
self.ldap_session.search(_lookedup_principal, '(distinguishedName=%s)' % _lookedup_principal, attributes=['objectSid'])
try:
self.principal_SID = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0])
except IndexError:
logging.error('Principal not found in LDAP (%s)' % _lookedup_principal)
return False
logging.debug("Found principal SID to write in ACE(s): %s" % self.principal_SID)
logging.info("Printing parsed DACL")
i = 0
# If a principal has been specified, only the ACE where he is the trustee will be printed
if self.principal_SID is not None:
logging.info("Filtering results for SID (%s)" % self.principal_SID)
for parsed_ace in parsed_dacl:
print_ace = True
if self.principal_SID is not None:
try:
if self.principal_SID not in parsed_ace['Trustee (SID)']:
print_ace = False
except Exception as e:
logging.error("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % e)
if print_ace:
logging.info(" %-28s" % "ACE[%d] info" % i)
self.printparsedACE(parsed_ace)
i += 1
# Prints properly a parsed ACE
# - parsed_ace : a parsed ACE from parseACE()
def printparsedACE(self, parsed_ace):
elements_name = list(parsed_ace.keys())
for attribute in elements_name:
logging.info(" %-26s: %s" % (attribute, parsed_ace[attribute]))
# Retrieves the GUIDs for the specified rights
def build_guids_for_rights(self):
_rights_guids = []
if self.rights_guid is not None:
_rights_guids = [self.rights_guid]
elif self.rights == "WriteMembers":
_rights_guids = [RIGHTS_GUID.WriteMembers.value]
elif self.rights == "ResetPassword":
_rights_guids = [RIGHTS_GUID.ResetPassword.value]
elif self.rights == "DCSync":
_rights_guids = [RIGHTS_GUID.DS_Replication_Get_Changes.value, RIGHTS_GUID.DS_Replication_Get_Changes_All.value]
logging.debug('Built GUID: %s', _rights_guids)
return _rights_guids
# Attempts to push the locally built DACL to the remote server into the security descriptor of the specified principal
# The target principal is specified with its Distinguished Name
# - dn : the principal's Distinguished Name to modify
# - secDesc : the Security Descriptor with the new DACL to push
def modify_secDesc_for_dn(self, dn, secDesc):
data = secDesc.getData()
controls = security_descriptor_control(sdflags=0x04)
logging.debug('Attempts to modify the Security Descriptor.')
self.ldap_session.modify(dn, {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [data])}, controls=controls)
if self.ldap_session.result['result'] == 0:
logging.info('DACL modified successfully!')
else:
if self.ldap_session.result['result'] == 50:
logging.error('Could not modify object, the server reports insufficient rights: %s',
self.ldap_session.result['message'])
elif self.ldap_session.result['result'] == 19:
logging.error('Could not modify object, the server reports a constrained violation: %s',
self.ldap_session.result['message'])
else:
logging.error('The server returned an error: %s', self.ldap_session.result['message'])
# Builds a standard ACE for a specified access mask (rights) and a specified SID (the principal who obtains the right)
# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/72e7c7ea-bc02-4c74-a619-818a16bf6adb
# - access_mask : the allowed access mask
# - sid : the principal's SID
# - ace_type : the ACE type (allowed or denied)
def create_ace(self, access_mask, sid, ace_type):
nace = ldaptypes.ACE()
if ace_type == "allowed":
nace['AceType'] = ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE
acedata = ldaptypes.ACCESS_ALLOWED_ACE()
else:
nace['AceType'] = ldaptypes.ACCESS_DENIED_ACE.ACE_TYPE
acedata = ldaptypes.ACCESS_DENIED_ACE()
if self.inheritance:
nace['AceFlags'] = ldaptypes.ACE.OBJECT_INHERIT_ACE + ldaptypes.ACE.CONTAINER_INHERIT_ACE
else:
nace['AceFlags'] = 0x00
acedata['Mask'] = ldaptypes.ACCESS_MASK()
acedata['Mask']['Mask'] = access_mask
acedata['Sid'] = ldaptypes.LDAP_SID()
acedata['Sid'].fromCanonical(sid)
nace['Ace'] = acedata
logging.debug('ACE created.')
return nace
# Builds an object-specific for a specified ObjectType (an extended right, a property, etc, to add) for a specified SID (the principal who obtains the right)
# The Mask is "ADS_RIGHT_DS_CONTROL_ACCESS" (the ObjectType GUID will identify an extended access right)
# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/c79a383c-2b3f-4655-abe7-dcbb7ce0cfbe
# - privguid : the ObjectType (an Extended Right here)
# - sid : the principal's SID
# - ace_type : the ACE type (allowed or denied)
def create_object_ace(self, privguid, sid, ace_type, force_mask=None):
nace = ldaptypes.ACE()
if ace_type == "allowed":
nace['AceType'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE
acedata = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE()
else:
nace['AceType'] = ldaptypes.ACCESS_DENIED_OBJECT_ACE.ACE_TYPE
acedata = ldaptypes.ACCESS_DENIED_OBJECT_ACE()
if self.inheritance:
nace['AceFlags'] = ldaptypes.ACE.OBJECT_INHERIT_ACE + ldaptypes.ACE.CONTAINER_INHERIT_ACE
else:
nace['AceFlags'] = 0x00
acedata['Mask'] = ldaptypes.ACCESS_MASK()
# WriteMembers not an extended right, we need read and write mask on the attribute (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/c79a383c-2b3f-4655-abe7-dcbb7ce0cfbe)
# force_mask in the case we give the -rights-guid option
if force_mask is not None:
acedata['Mask']['Mask'] = force_mask
elif privguid == RIGHTS_GUID.WriteMembers.value:
acedata['Mask']['Mask'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_READ_PROP + ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP
# Other rights in this script are extended rights and need the DS_CONTROL_ACCESS mask
else:
acedata['Mask']['Mask'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS
acedata['ObjectType'] = string_to_bin(privguid)
acedata['InheritedObjectType'] = b''
acedata['Sid'] = ldaptypes.LDAP_SID()
acedata['Sid'].fromCanonical(sid)
assert sid == acedata['Sid'].formatCanonical()
# This ACE flag verifes if the ObjectType is valid
acedata['Flags'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT
nace['Ace'] = acedata
logging.debug('Object-specific ACE created.')
return nace
def parse_args():
parser = argparse.ArgumentParser(add_help=True, description='Python editor for a principal\'s DACL.')
parser.add_argument('identity', action='store', help='domain.local/username[:password]')
parser.add_argument('-use-ldaps', action='store_true', help='Use LDAPS instead of LDAP')
parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output')
parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')
auth_con = parser.add_argument_group('authentication & connection')
auth_con.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH')
auth_con.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)')
auth_con.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ones specified in the command line')
auth_con.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication (128 or 256 bits)')
auth_con.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted it will use the domain part (FQDN) specified in the identity parameter')
auth_con.add_argument('-dc-host', action='store', metavar="hostname", help='Hostname of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted, -dc-ip will be used')
principal_parser = parser.add_argument_group("principal", description="Object, controlled by the attacker, to reference in the ACE to create or to filter when printing a DACL")
principal_parser.add_argument("-principal", dest="principal_sAMAccountName", metavar="NAME", type=str, required=False, help="sAMAccountName")
principal_parser.add_argument("-principal-sid", dest="principal_SID", metavar="SID", type=str, required=False, help="Security IDentifier")
principal_parser.add_argument("-principal-dn", dest="principal_DN", metavar="DN", type=str, required=False, help="Distinguished Name")
target_parser = parser.add_argument_group("target", description="Principal object to read/edit the DACL of")
target_parser.add_argument("-target", dest="target_sAMAccountName", metavar="NAME", type=str, required=False, help="sAMAccountName")
target_parser.add_argument("-target-sid", dest="target_SID", metavar="SID", type=str, required=False, help="Security IDentifier")
target_parser.add_argument("-target-dn", dest="target_DN", metavar="DN", type=str, required=False, help="Distinguished Name")
dacl_parser = parser.add_argument_group("dacl editor")
dacl_parser.add_argument('-action', choices=['read', 'write', 'remove', 'backup', 'restore'], nargs='?', default='read', help='Action to operate on the DACL')
dacl_parser.add_argument('-file', dest="filename", type=str, help='Filename/path (optional for -action backup, required for -restore))')
dacl_parser.add_argument('-ace-type', choices=['allowed', 'denied'], nargs='?', default='allowed', help='The ACE Type (access allowed or denied) that must be added or removed (default: allowed)')
dacl_parser.add_argument('-rights', choices=['FullControl', 'ResetPassword', 'WriteMembers', 'DCSync', 'Custom'], nargs='?', default='FullControl', help='Rights to write/remove in the target DACL (default: FullControl)')
dacl_parser.add_argument('-rights-guid', type=str, help='Manual GUID representing the right to write/remove')
dacl_parser.add_argument('-mask', nargs='?', default=None, help='Force access mask, possible values: readwrite, write, self, allext, 0xXXXXX. Useful with -rights Custom or --rights-guid where the mask is different of read+write.')
dacl_parser.add_argument('-inheritance', action="store_true", help='Enable the inheritance in the ACE flag with CONTAINER_INHERIT_ACE and OBJECT_INHERIT_ACE. Useful when target is a Container or an OU, '
'ACE will be inherited by objects within the container/OU (except objects with adminCount=1)')
if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)
return parser.parse_args()
def main():
print(version.BANNER)
args = parse_args()
logger.init(args.ts, args.debug)
if args.action == 'write' and args.principal_sAMAccountName is None and args.principal_SID is None and args.principal_DN is None:
logging.critical('-principal, -principal-sid, or -principal-dn should be specified when using -action write')
sys.exit(1)
if args.action == "restore" and not args.filename:
logging.critical('-file is required when using -action restore')
domain, username, password, lmhash, nthash, args.k = parse_identity(args.identity, args.hashes, args.no_pass, args.aesKey, args.k)
try:
ldap_server, ldap_session = init_ldap_session(domain, username, password, lmhash, nthash, args.k, args.dc_ip, args.dc_host, args.aesKey, args.use_ldaps)
dacledit = DACLedit(ldap_server, ldap_session, args)
if args.action == 'read':
dacledit.read()
elif args.action == 'write':
dacledit.write()
elif args.action == 'remove':
dacledit.remove()
elif args.action == 'flush':
dacledit.flush()
elif args.action == 'backup':
dacledit.backup()
elif args.action == 'restore':
dacledit.restore()
except Exception as e:
if logging.getLogger().level == logging.DEBUG:
traceback.print_exc()
logging.error(str(e))
if __name__ == '__main__':
main()
|