Active Directory Sites and Subnets enumeration
Introduction
In an Active Directory domain, you can configure many settings for small or huge organizations. In large AD domains, it is quite common to have many IT infrastructures spread across different geographical locations. In order to manage theses configurations ADDS provides a tool called Active Directory Sites and Services allowing administrators to manage large domains across many sites.
From an attacker point of view, it is a very important part of the recognition phase of a pentest to understand which machines can be accessed within which subnets. With this information an attacker can then choose the best next targets from the already compromised machines.
Let’s see how Active Directory Sites and Services works, and write a script to enumerate these from Windows in Powershell, and a crackmapexec module to use it from linux.
Active Directory Sites and Services
From an administrator point of view, the Active Directory Sites and Services console looks like this:
All these information are stored in the LDAP:
- The AD Sites are stored in:
CN=Configuration,DC=LAB,DC=local
- The Subnets are stored in:
CN=Sites,CN=Configuration,DC=LAB,DC=local
- The Servers are stored in the site distinguishedName:
%s,DC=LAB,DC=local
Enumeration
To enumerate these properties from an Active Directory, we need:
- To have valid credentials for the domain
- To be able to query the LDAP
With this in mind, we can construct a pseudo-code for extracting these informations. Firstly we need to extract the sites of the forest. Then we will extract the subnets of the site, and the servers declared inside the site. This gives us the follwing pseudo-code:
- Extract sites of the forest
- Foreach site in sites:
- Extract subnets of the site
- Foreach subnet in subnets:
- Extract servers of the site
- Foreach server in servers:
- Print found server
In Powershell from windows
We just have to translate this pseudo-code in Powershell, and now you can enumerate sites, subnets and servers in powershell with these few lines:
$sites = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().Sites
foreach ($site in $sites) {
write-host "[>] (site) $site"
foreach ($subnet in $site.Subnets) {
write-host " └─> (subnet) $subnet"
foreach ($server in $site.Servers) {
write-host " └─> (server) $server"
}
}
}
This gives us a similar tree output to the Active Directory Sites and Services console:
Or in oneline :
foreach ($s in [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().Sites ){write-host "[>] (site) $s";foreach ($r in $s.Subnets){write-host " └─> (subnet) $r";foreach ($m in $s.Servers){write-host " └─> (server) $m"}}}
Using crackmapexec from linux
I wrote a crackmapexec module to use it from linux, based on the same pseudo-code. Here is an example of the module output:
To do this I used LDAP queries to get all the sites, then another LDAP query to get all the subnet of that site, and an LDAP query to get all the Servers of the site. This is basically the same as what we did in Powershell earlier, but without Windows builtin functions.
This module was proposed as a pull request #509, in the meantime here is the final module code:
from impacket.ldap import ldapasn1 as ldapasn1_impacket
def searchResEntry_to_dict(results):
data = {}
for attr in results['attributes']:
key = str(attr['type'])
value = str(attr['vals'][0])
data[key] = value
return data
class CMEModule:
'''
Retrieves the different Sites and Subnets of an Active Directory
Authors:
Podalirius: @podalirius_
'''
def options(self, context, module_options):
pass
name = 'subnets'
description = 'Retrieves the different Sites and Subnets of an Active Directory'
supported_protocols = ['ldap']
opsec_safe = True
multiple_hosts = False
def on_login(self, context, connection):
dn = ','.join(["DC=%s" % part for part in context.domain.split('.')])
context.log.info('Getting the Sites and Subnets from domain')
list_sites = connection.ldapConnection.search(
searchBase="CN=Configuration,%s" % dn,
searchFilter='(objectClass=site)',
attributes=['distinguishedName', 'name', 'description'],
sizeLimit=999
)
for site in list_sites:
if isinstance(site, ldapasn1_impacket.SearchResultEntry) is not True:
continue
site = searchResEntry_to_dict(site)
site_dn = site['distinguishedName']
site_name = site['name']
site_description = ""
if "description" in site.keys():
site_description = site['description']
# Getting subnets of this site
list_subnets = connection.ldapConnection.search(
searchBase="CN=Sites,CN=Configuration,%s" % dn,
searchFilter='(siteObject=%s)' % site_dn,
attributes=['distinguishedName', 'name', 'description'],
sizeLimit=999
)
for subnet in list_subnets:
if isinstance(subnet, ldapasn1_impacket.SearchResultEntry) is not True:
continue
subnet = searchResEntry_to_dict(subnet)
subnet_dn = subnet['distinguishedName']
subnet_name = subnet['name']
subnet_description = ""
if "description" in subnet.keys():
site_description = subnet['description']
context.log.highlight(" │ Site (Name:%s) (Subnet:%s)" % (site_name, subnet_name))
if len(site_description) != 0:
context.log.highlight(' │ │ Site description: \"%s\"' % (site_description))
if len(subnet_description) != 0:
context.log.highlight(' │ │ Subnet description: \"%s\"' % (subnet_description))
# Getting machines in these subnets
list_servers = connection.ldapConnection.search(
searchBase=site_dn,
searchFilter='(objectClass=server)',
attributes=['cn'],
sizeLimit=999
)
for server in list_servers:
if isinstance(server, ldapasn1_impacket.SearchResultEntry) is not True:
continue
server = searchResEntry_to_dict(server)['cn']
context.log.highlight(" │ ├───> Server: %s" % server)