Active Directory Sites and Subnets enumeration

Table of contents :

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)

References