-
-
Save dirkjanm/a2087c27888a15410b4622009c7f2d41 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python | |
#################### | |
# | |
# Copyright (c) 2018 Dirk-jan Mollema - Fox-IT | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy | |
# of this software and associated documentation files (the "Software"), to deal | |
# in the Software without restriction, including without limitation the rights | |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
# copies of the Software, and to permit persons to whom the Software is | |
# furnished to do so, subject to the following conditions: | |
# | |
# The above copyright notice and this permission notice shall be included in all | |
# copies or substantial portions of the Software. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
# SOFTWARE. | |
# | |
# Test nTSecurityDescriptor encoding/decoding for impacket | |
# | |
# Uses impacket and ldap3 | |
# Install impacket from git, and ldap3 via pip | |
# | |
#################### | |
import sys | |
import argparse | |
import ldapdomaindump | |
import impacket | |
import getpass | |
import binascii | |
from impacket.ldap.ldaptypes import ACE, ACCESS_ALLOWED_OBJECT_ACE, ACCESS_MASK, LDAP_SID, SR_SECURITY_DESCRIPTOR | |
from struct import unpack, pack | |
from ldap3 import NTLM, Server, Connection, ALL, LEVEL, MODIFY_REPLACE | |
from ldap3.core.exceptions import LDAPProtocolErrorResult | |
from pyasn1.type.namedtype import NamedTypes, NamedType | |
from pyasn1.type.univ import Sequence, OctetString, Integer | |
from ldap3.protocol.controls import build_control | |
from impacket.uuid import string_to_bin | |
class SdFlags(Sequence): | |
# SDFlagsRequestValue ::= SEQUENCE { | |
# Flags INTEGER | |
# } | |
componentType = NamedTypes(NamedType('Flags', Integer()) | |
) | |
def get_sd_controls(sdflags=0x04): | |
sdcontrol = SdFlags() | |
sdcontrol.setComponentByName('Flags', sdflags) | |
controls = [build_control('1.2.840.113556.1.4.801', True, sdcontrol)] | |
return controls | |
def create_object_ace(privguid, sid): | |
nace = ACE() | |
nace['AceType'] = ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE | |
nace['AceFlags'] = 0x00 | |
acedata = ACCESS_ALLOWED_OBJECT_ACE() | |
acedata['Mask'] = ACCESS_MASK() | |
acedata['Mask']['Mask'] = ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS | |
acedata['ObjectType'] = string_to_bin(privguid) | |
acedata['InheritedObjectType'] = '' | |
acedata['Sid'] = LDAP_SID() | |
acedata['Sid'].fromCanonical(sid) | |
acedata['Flags'] = ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT | |
nace['Ace'] = acedata | |
return nace | |
def print_m(string): | |
sys.stderr.write('\033[94m[-]\033[0m %s\n' % (string)) | |
def print_o(string): | |
sys.stderr.write('\033[92m[+]\033[0m %s\n' % (string)) | |
def print_f(string): | |
sys.stderr.write('\033[91m[!]\033[0m %s\n' % (string)) | |
def main(): | |
parser = argparse.ArgumentParser(description='Test nTSecurityDescriptor encoding/decoding for impacket') | |
parser._optionals.title = "Main options" | |
parser._positionals.title = "Required options" | |
#Main parameters | |
#maingroup = parser.add_argument_group("Main options") | |
parser.add_argument("host", type=str,metavar='HOSTNAME',help="Hostname/ip or ldap://host:port connection string to connect to") | |
parser.add_argument("-u","--user",type=str,metavar='USERNAME',help="DOMAIN\username for authentication, leave empty for anonymous authentication") | |
parser.add_argument("-p","--password",type=str,metavar='PASSWORD',help="Password or LM:NTLM hash, will prompt if not specified") | |
args = parser.parse_args() | |
#Prompt for password if not set | |
authentication = None | |
if args.user is not None: | |
authentication = NTLM | |
if not '\\' in args.user: | |
print_f('Username must include a domain, use: DOMAIN\username') | |
sys.exit(1) | |
if args.password is None: | |
args.password = getpass.getpass() | |
# define the server and the connection | |
s = Server(args.host, get_info=ALL) | |
print_m('Connecting to host...') | |
c = Connection(s, user=args.user, password=args.password, authentication=authentication) | |
print_m('Binding to host') | |
# perform the Bind operation | |
if not c.bind(): | |
print_f('Could not bind with specified credentials') | |
print_f(c.result) | |
sys.exit(1) | |
rootdn = s.info.other['rootDomainNamingContext'][0] | |
print_o('Bind OK') | |
perms = lookup_permissions(c, rootdn) | |
controls = get_sd_controls() | |
entries = c.extend.standard.paged_search(rootdn, '(objectClass=*)', attributes=['nTSecurityDescriptor'], controls=controls, generator=True) | |
print_m('Will now enumerate all domain objects. This may take a while depending on the domain size.') | |
impacket.ldap.ldaptypes.RECALC_ACE_SIZE = False | |
c_all = 0 | |
c_ok = 0 | |
c_fail = 0 | |
c_no_access = 0 | |
while True: | |
try: | |
e = entries.next() | |
except LDAPProtocolErrorResult: | |
pass | |
except StopIteration: | |
break | |
if e['type'] != 'searchResEntry': | |
continue | |
dn = e['dn'] | |
c_all += 1 | |
if c_all % 100 == 0: | |
print_m('Processed %d objects' % c_all) | |
try: | |
sd = e['raw_attributes']['nTSecurityDescriptor'][0] | |
except IndexError: | |
c_no_access += 1 | |
continue | |
if len(controls) > 1: | |
controls.pop() | |
a = SR_SECURITY_DESCRIPTOR() | |
a.fromString(sd) | |
b = a.getData() | |
if b == sd: | |
c_ok += 1 | |
else: | |
c_fail += 1 | |
print 'Failed: %d' % c_fail | |
print 'Ok: %d' % c_ok | |
print 'No access (or empty SD): %d' % c_no_access | |
if c_fail == 0: | |
print_o('No objects failed to encode/decode, yay!') | |
else: | |
print_f('Whoops, not all objects were encoded/decoded succesfully') | |
def lookup_permissions(ldapconnection, rootdn): | |
ldapconnection.extend.standard.paged_search('CN=Extended-Rights,CN=Configuration,%s' % rootdn, '(rightsGuid=*)', search_scope=LEVEL, attributes=['displayName','rightsGuid'], paged_size=500, generator=False) | |
ep = {} | |
for e in ldapconnection.entries: | |
ep[e['rightsGuid'].values[0].lower()] = e['displayName'].values[0] | |
ldapconnection.extend.standard.paged_search('CN=Schema,CN=Configuration,%s' % rootdn, '(schemaIdGuid=*)', search_scope=LEVEL, attributes=['name','schemaIdGuid'], paged_size=500, generator=False) | |
for e in ldapconnection.entries: | |
ep[e['schemaIdGuid'].values[0].lower()] = e['name'].values[0] | |
return ep | |
if __name__ == '__main__': | |
main() |
Unable to decode this kind of ntsecuritydescriptor b'0100049c000000000000000000000000140000000400d4000500000005003800300100000100000068c9100efb78d21190d400c04f79dc550105000000000005150000009328446371b3986185a90c5c0002000005003800300100000100000068c9100efb78d21190d400c04f79dc550105000000000005150000009328446371b3986185a90c5c0702000000002400ff000f000105000000000005150000009328446371b3986185a90c5c0002000000002400ff000f000105000000000005150000009328446371b3986185a90c5c07020000000014009400020001010000000000050b00' on python3
on sd.fromString(secDesc)
Getting exception as
struct.error: ('unpack requires a buffer of 1 bytes', "When unpacking field 'Revision | <B | b''[:1]'") Same data works perfectly when I use C# RawSecurityDescriptor. Am I doing something silly ?
Did you solve this im having a similar issue
if secDesc[0:2] == '0x':
secDesc = secDesc[2:]
sd.fromString(binascii.unhexlify(secDesc)) #worked for me.
Awesome! The get_sd_controls function gave me what I needed to retrieve the DACLs from AD objects using a non-privileged account. Thanks :)
Unable to decode this kind of ntsecuritydescriptor
b'0100049c000000000000000000000000140000000400d4000500000005003800300100000100000068c9100efb78d21190d400c04f79dc550105000000000005150000009328446371b3986185a90c5c0002000005003800300100000100000068c9100efb78d21190d400c04f79dc550105000000000005150000009328446371b3986185a90c5c0702000000002400ff000f000105000000000005150000009328446371b3986185a90c5c0002000000002400ff000f000105000000000005150000009328446371b3986185a90c5c07020000000014009400020001010000000000050b00' on python3
on sd.fromString(secDesc)
Getting exception as
struct.error: ('unpack requires a buffer of 1 bytes', "When unpacking field 'Revision | <B | b''[:1]'")
Same data works perfectly when I use C# RawSecurityDescriptor. Am I doing something silly ?