Enumération des sites et sous réseaux Active Directory

Table des matières :

Introduction

Dans un domaine Active Directory, vous pouvez configurer de nombreux paramètres pour les petites ou les grandes organisations. Dans les grands domaines AD, il est assez courant d’avoir de nombreuses infrastructures informatiques réparties sur différents emplacements géographiques. Afin de gérer ces configurations, ADDS fournit un outil appelé sites et services Active Directory permettant aux administrateurs de gérer de grands domaines sur de nombreux sites.

Du point de vue d’un attaquant, c’est une partie très importante de la phase de reconnaissance d’un pentest de comprendre quelles machines sont accessibles dans quels sous-réseaux. Avec ces informations, un attaquant peut alors choisir les meilleures prochaines cibles parmi les machines déjà compromises.

Nous allons voir comment fonctionnent les sites et services d’Active Directory et écrire un script pour les énumérer à partir de Windows avec Powershell. Ensuite, nous verrons un module crackmapexec pour l’utiliser à partir de Linux.

Active Directory Sites and Services

Du point de vue de l’administrateur, la console Sites et services Active Directory ressemble à ceci :

Toutes ces informations sont stockées dans le LDAP :

  • Les AD Sites sont stockés dans: CN=Configuration,DC=LAB,DC=local
  • Les Subnets sont stockés dans: CN=Sites,CN=Configuration,DC=LAB,DC=local
  • Les Serveurs sont stockés dans le distinguishedName du site, par exemple: %s,DC=LAB,DC=local

Énumération

Pour énumérer ces propriétés à partir d’un Active Directory, nous avons besoin de :

  • Avoir des identifiants valides pour le domaine
  • Pouvoir interroger le LDAP

Dans cette optique, nous pouvons construire un pseudo-code pour extraire ces informations. Premièrement, nous devons extraire les sites de la forêt. Ensuite, nous extrairons les sous-réseaux du site, et les serveurs déclarés à l’intérieur du site. Cela nous donne le pseudo-code suivant :

  • Extraire les sites de la forêt
  • Pour chaque site dans sites :
    • Extraire les sous-réseaux du site
    • Pour chaque sous-réseau dans les sous-réseaux :
      • Extraire les serveurs du site
      • Pour chaque serverur dans les serveurs :
        • Afficher le serveur trouvé

Dans Powershell à partir de Windows

Il ne nous reste plus qu’à traduire ce pseudo-code en Powershell, et maintenant vous pouvez énumérer les sites, sous-réseaux et serveurs en powershell avec ces quelques lignes :

$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"
        }
    }
}

Cela nous donne une sortie d’arborescence similaire à la console Sites et services Active Directory :

Ou en une seule ligne :

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"}}}

Utilisation de crackmapexec de Linux

J’ai écrit un module crackmapexec pour l’utiliser depuis Linux, basé sur le même pseudo-code. Voici un exemple de la sortie du module :

Pour ce faire, j’ai utilisé des requêtes LDAP pour obtenir tous les sites, puis une autre requête LDAP pour obtenir tout le sous-réseau de ce site et une requête LDAP pour obtenir tous les serveurs du site. C’est fondamentalement la même chose que ce que nous avons fait précédemment dans Powershell, mais sans les fonctions intégrées de Windows.

Ce module a été proposé en tant que pull request #509, en attendant voici le code final du module :

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)

Références