Skip to content

Commit 84cbdad

Browse files
author
SkelSec
committed
update version to 0.5.15; enhance MSLDAPClient with domain SID and name attributes, add methods for managing DMSA accounts, and improve security descriptor handling
1 parent 6739484 commit 84cbdad

File tree

6 files changed

+530
-9
lines changed

6 files changed

+530
-9
lines changed

msldap/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
__version__ = "0.5.14"
2+
__version__ = "0.5.15"
33
__banner__ = \
44
"""
55
# msldap %s

msldap/client.py

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from winacl.dtyp.ace import ACCESS_ALLOWED_OBJECT_ACE, ADS_ACCESS_MASK
1515
from winacl.dtyp.guid import GUID
1616
from winacl.dtyp.security_descriptor import SECURITY_DESCRIPTOR
17+
from winacl.dtyp.ace import ACCESS_ALLOWED_OBJECT_ACE
1718
from winacl.dtyp.sid import SID
1819

1920
from msldap import logger
@@ -70,6 +71,8 @@ def __init__(self, target:MSLDAPTarget, creds:UniCredential, connection = None,
7071
self.disconnected_evt = None
7172
self._sid_cache = {} #SID -> (domain, user)
7273
self._domainsid_cache = {} # SID -> domain
74+
self.domainname = None
75+
self.domainsid = None
7376

7477

7578
async def __aenter__(self):
@@ -138,7 +141,8 @@ async def connect(self):
138141
if err is not None:
139142
raise err
140143
self._domainsid_cache[self._ldapinfo.objectSid] = self._ldapinfo.name
141-
144+
self.domainname = self._ldapinfo.name
145+
self.domainsid = self._ldapinfo.objectSid
142146
if self.keepalive is True:
143147
self.__keepalive_task = asyncio.create_task(self.__keepalive(res['defaultNamingContext']))
144148
return True, None
@@ -149,11 +153,13 @@ def get_server_info(self):
149153
return self._serverinfo
150154

151155
async def get_domain_name(self):
156+
if self.domainname is not None:
157+
return self.domainname, None
152158
self._ldapinfo, err = await self.get_ad_info()
153159
if err is not None:
154160
return None, err
155-
domain = self._ldapinfo.name
156-
return domain, err
161+
self.domainname = self._ldapinfo.name
162+
return self.domainname, err
157163

158164

159165
async def pagedsearch(self, query:str, attributes:List[str], controls:List[Tuple[str, str, str]] = None, tree:str = None, search_scope:int=2):
@@ -719,6 +725,24 @@ async def get_user_by_dn(self, user_dn:str):
719725
if err is not None:
720726
return None, err
721727
return MSADUser.from_ldap(entry), None
728+
729+
return None, Exception('Search returned no results!')
730+
731+
async def get_user_by_sid(self, sid:str):
732+
"""
733+
Fetches the DN for an object specified by `sid`
734+
735+
:param sid: The user's SID
736+
:type sid: str
737+
"""
738+
739+
ldap_filter = r'(objectSid=%s)' % str(sid)
740+
async for entry, err in self.pagedsearch(ldap_filter, MSADUser_ATTRS):
741+
if err is not None:
742+
return None, err
743+
return MSADUser.from_ldap(entry), None
744+
745+
return None, Exception('Search returned no results!')
722746

723747
async def get_group_members(self, dn:str, recursive:bool = False):
724748
"""
@@ -963,6 +987,30 @@ async def create_user_dn(self, user_dn:str, password:str):
963987
except Exception as e:
964988
return False, e
965989

990+
async def create_broken_dmsa_user(self, user_dn:str, computer_sid:str):
991+
"""
992+
This will create a dmsa service user that can be used for neferious reasons, but DO NOT USE THIS FOR ANYTHING ELSE!
993+
994+
"""
995+
try:
996+
sn = user_dn.split(',')[0][3:]
997+
attributes = {
998+
'objectClass': ['msDS-DelegatedManagedServiceAccount','organizationalPerson', 'person', 'top', 'user', 'computer'],
999+
'sn': sn,
1000+
'sAMAccountName': sn,
1001+
'displayName': sn,
1002+
'msDS-DelegatedMSAState' : 0,
1003+
'msDS-ManagedPasswordInterval': 30,
1004+
'msDS-SupportedEncryptionTypes': 28,
1005+
'userAccountControl': 4096,
1006+
'msDS-GroupMSAMembership': SECURITY_DESCRIPTOR.from_sddl('O:S-1-5-32-544D:(A;;0xf01ff;;;%s)' % computer_sid.upper())
1007+
}
1008+
_, err = await self._con.add(user_dn, attributes)
1009+
if err is not None:
1010+
return False, err
1011+
return True, None
1012+
except Exception as e:
1013+
return False, e
9661014

9671015
async def unlock_user(self, user_dn:str):
9681016
"""
@@ -1789,7 +1837,59 @@ async def dnsentries(self, zone = None, with_tombstones = False):
17891837
except Exception as e:
17901838
yield None, None, None, e
17911839

1792-
1840+
async def get_all_domain_controllers(self):
1841+
"""Lists all domain controllers in the forest"""
1842+
# domain controller is a machine account that has the flag MSLDAP_UAC.SERVER_TRUST_ACCOUNT
1843+
ldap_filter = r'(&(sAMAccountType=805306369)(userAccountControl:1.2.840.113556.1.4.803:=8192))'
1844+
async for entry, err in self.pagedsearch(ldap_filter, MSADMachine_ATTRS):
1845+
if err is not None:
1846+
yield None, err
1847+
continue
1848+
yield MSADMachine.from_ldap(entry), None
1849+
1850+
async def get_all_dmsas(self):
1851+
"""Lists all DGMAs in the forest"""
1852+
async for entry, err in self.pagedsearch(r'(objectClass=msDS-DelegatedManagedServiceAccount)', MSADDMSAUser_ATTRS):
1853+
if err is not None:
1854+
raise err
1855+
yield MSADDMSAUser.from_ldap(entry), None
1856+
1857+
async def get_obj_by_dn(self, dn:str, class_type:str):
1858+
"""
1859+
Fetches one object from the AD, based on the DN attribute
1860+
1861+
:param dn: The DN of the object.
1862+
:type dn: str
1863+
:param class_type: The class of the object.
1864+
:type class_type: str
1865+
:return: A tuple with the object and an `Exception` is there was any
1866+
:rtype: (:class:`MSADUser`, :class:`Exception`)
1867+
"""
1868+
if class_type == 'user':
1869+
oc = MSADUser
1870+
elif class_type == 'computer':
1871+
oc = MSADMachine
1872+
elif class_type == 'group':
1873+
oc = MSADGroup
1874+
elif class_type == 'container':
1875+
oc = MSADContainer
1876+
elif class_type == 'dmsa':
1877+
oc = MSADDMSAUser
1878+
else:
1879+
oc = None
1880+
1881+
logger.debug('Polling AD for object %s'% dn)
1882+
ldap_filter = r'(distinguishedName=%s)' % dn
1883+
async for entry, err in self.pagedsearch(ldap_filter, MSADUser_ATTRS):
1884+
if err is not None:
1885+
return None, err
1886+
if oc is not None:
1887+
return oc.from_ldap(entry, self._ldapinfo), None
1888+
else:
1889+
return entry, None
1890+
else:
1891+
return None, None
1892+
logger.debug('Finished polling for entries!')
17931893

17941894
#async def get_permissions_for_dn(self, dn):
17951895
# """

0 commit comments

Comments
 (0)