How to get the Organization Units (OU) and Hosts from Microsoft Active Directory using Python ldap3
Alexander Leonov
Vulnerability & Compliance Management, Security Automation, Metrics
I recently figured out how to work with Microsoft Active Directory using Python 3. I wanted to get a hierarchy of Organizational Units (OUs) and all the network hosts associated with these OUs to search for possible anomalies. If you are not familiar with AD, here is a good thread about the difference between AD Group and OU.
It seems much easier to solve such tasks using PowerShell. But it will probably require a Windows server. So I leave this for the worst scenario. ?? There is also a PowerShell Core, which should support Linux, but I haven’t tried it yet. If you want to use Python, there is a choice from the native python ldap3 module and Python-ldap, which is a wrapper for the OpenLDAP client. I didn’t find any interesting high-level functions in Python-ldap and finally decided to use ldap3.
Connection
At first you will need to create a Connection to the server, which can be used in further requests:
#!/usr/bin/python
# -*- coding: utf-8 -*-
from ldap3 import Server, Connection, SUBTREE, LEVEL
server = Server("ad-server.corporation.com", port=389, use_ssl=False, get_info='ALL')
connection = Connection(server, user="user1", password="Password123",
fast_decoder=True, auto_bind=True, auto_referrals=True, check_names=False, read_only=True,
lazy=False, raise_exceptions=False)
When the connection is no longer needed, run:
connection.unbind()
Getting the child OUs
I did not find how to get the whole tree of Organizational Units using one high-level command. But I found out how to make a function, that returns all child OUs (basically their domain names) for the particular OU (domain name):
def get_child_ou_dns(dn, connection):
results = list()
elements = connection.extend.standard.paged_search(
search_base=dn,
search_filter='(objectCategory=organizationalUnit)',
search_scope=LEVEL,
paged_size=100)
for element in elements:
if 'dn' in element:
if element['dn'] != dn:
if 'dn' in element:
results.append(element['dn'])
return(results)
Note the great feature of ldap3: with conn.extend.standard.paged_search you can search in AD without worrying about pagination. It’s very convenient. I also added if element['dn'] != dn, because, for some reason, the response may contain the element with the same dn that was set in the parameters of the function, so I skip it.
Example:
print(get_child_ou_dns(dn="OU=Computers Office,DC=corporation,DC=com", connection=connection))
Output:
[u'OU=NY,OU=Computers Office,DC=corporation,DC=com', u'OU=SLC,OU=Computers Office,DC=corporation,DC=com',
u'OU=London,OU=Computers Office,DC=corporation,DC=com', ...]
Getting the hierarchy of OUs
As we see above, the entire hierarchy is contained in dn:
OU=NY,OU=Computers Office,DC=corporation,DC=com DC=com, DC=corporation -> OU=Computers Office -> OU=NY
You can get all the OUs by setting search_base='DC=corporation,DC=com' and search_scope=SUBTREEin the search request and restore the hierarchy by parsing the DNs. But, it seem more reliable to do this through the sequence of searches. Because it is not clear how the DN is generated and a garbage we can meet there.
Instead of this, to get the whole tree of Organizational Units I go down through the all OUs starting from kernel (‘DC=corporation,DC=com’). It can be done with recursion, but I used cycles, because I think it’s more clear this way.