How to hunt for LDAP reconnaissance within M365 Defender?

Lightweight Directory Access Protocol (LDAP) is one of the core protocols used for directory services. The primary function of LDAP is to enable folks to find data about users, groups, computers, and much more. It also provides the communication language that applications require to send and receive information from directory services, such as Active Directory. In overall, LDAP is the protocol to communicate within a directory service.

Adversaries can use the LDAP protocol to perform reconnaissance and gather information that is stored within Active Directory to find attack paths and sensitive accounts with high-privileges. Discovering such kind of reconnaissance activities in an early stage will benefit defenders in stopping a potential intrusion.

This blog post has been inspired by an article from Microsoft, which can be found here. I just wanted to give a follow-up by adding some additional information to it and go a bit further with explaining on how we can run LDAP queries by ourselves.

Content

  • Determine whether it is LDAP reconnaissance activity or not
  • Executing LDAP queries by ourselves with ADFind (to understand it better)
  • OpSec mistakes attackers make when doing LDAP reconnaissance
  • How to make less noise when doing LDAP reconnaissance?
  • LDAP reconnaissance against Microsoft Defender for Identity
  • Advanced Hunting Queries in MDE to hunt for suspicious LDAP search filters

LDAP Reconnaissance Activities

A common tool adversaries are using is BloodHound, which uses SharpHound to collect various of data. SharpHound uses LDAP queries to collect information within Active Directory.

Once we ran this data collector, we can see that Microsoft Defender for Endpoint has captured all the LDAP queries that have been ran.

Determine whether true or false positive

After we received the alerts, we have to determine whether it is suspicious behavior or not. The great thing is that we can see which LDAP query was ran during the time.

In this example, we can see that, after we ran SharpHound. It actually runs the following LDAP query under the hood:

(|(|(samaccounttype=268435456)(samaccounttype=268435457)(samaccounttype=536870912)(samaccounttype=536870913)(primarygroupid=*))(&(sAMAccountType=805306369)(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))(|(samAccountType=805306368)(samAccountType=805306369)(samAccountType=268435456)(samAccountType=268435457)(samAccountType=536870912)(samAccountType=536870913)(objectClass=domain)(&(objectcategory=groupPolicyContainer)(flags=*))(objectcategory=organizationalUnit))(objectclass=domain)(|(samaccounttype=268435456)(samaccounttype=268435457)(samaccounttype=536870912)(samaccounttype=536870913)(samaccounttype=805306368)(samaccounttype=805306369)(objectclass=domain)(objectclass=organizationalUnit)(&(objectcategory=groupPolicyContainer)(flags=*)))(|(&(&(objectcategory=groupPolicyContainer)(flags=*))(name=*)(gpcfilesyspath=*))(objectcategory=organizationalUnit)(objectClass=domain))(&(samaccounttype=805306368)(serviceprincipalname=*)))

Now we have to understand what data SharpHound collects, so we will look the above LDAP query and split every LDAP search filter into different pieces.

Search FilterDescription
(&(samaccounttype=268435456)) All the security groups in AD
(&(samaccounttype=268435457))All the non security groups
(&(samaccounttype=536870912))All the groups in the Builtin container
(&(samaccounttype=536870913)) NON_SECURITY_ALIAS_OBJECT
(&(primarygroupid=*)) Contains the relative identifier (RID) for the primary group of the user
(&(sAMAccountType=805306369) All the machine accounts in AD
(!(UserAccountControl:1.2.840.113556.1.4.803:=2)) All the accounts that are enabled in AD
(|(samAccountType=805306368))All the accounts in AD, including the one’s that are disabled
( (objectClass=domain) )All the domains
(&(objectcategory=groupPolicyContainer))All the Group Policy Objects
(& (objectcategory=organizationalUnit))All the organizational units
(& (serviceprincipalname=*))All the accounts with a SPN

Advanced Hunting provides great visibility in all the LDAP search filters that have been ran by a process. However, it is very rare, that a process is running an LDAP query with multiple search filters in one request. This may indicate reconnaissance activities of an attacker.

An LDAP query with multiple search filters in one request happens rarely, so it might be an indication of an attacker performing reconnaissance.

How to run LDAP queries?

Adversaries have been using LDAP to perform reconnaissance, so if we want to understand the techniques of the attackers. We have to do the same thing, as they do. This helps us to understand it much better, which is doing it in practice.

In order to run the LDAP queries, we are going to use the famous tool ADFind.

  • Enumerate all the same data like SharpHound does

Command

This LDAP search filter will collect all the accounts, SPNs, group memberships, domains, GPOs, etc. Basically the same as what SharpHound collects.

AdFind.exe -default -f "(|(|(samaccounttype=268435456)(samaccounttype=268435457)(samaccounttype=536870912)(samaccounttype=536870913)(primarygroupid=*))(&(sAMAccountType=805306369)(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))(|(samAccountType=805306368)(samAccountType=805306369)(samAccountType=268435456)(samAccountType=268435457)(samAccountType=536870912)(samAccountType=536870913)(objectClass=domain)(&(objectcategory=groupPolicyContainer)(flags=*))(objectcategory=organizationalUnit))(objectclass=domain)(|(samaccounttype=268435456)(samaccounttype=268435457)(samaccounttype=536870912)(samaccounttype=536870913)(samaccounttype=805306368)(samaccounttype=805306369)(objectclass=domain)(objectclass=organizationalUnit)(&(objectcategory=groupPolicyContainer)(flags=*)))(|(&(&(objectcategory=groupPolicyContainer)(flags=*))(name=*)(gpcfilesyspath=*))(objectcategory=organizationalUnit)(objectClass=domain))(&(samaccounttype=805306368)(serviceprincipalname=*)))"

Result

At the sample result, we can see that 3396 objects being returned with various data from all the group memberships to the GPOs, and so on.

  • Enumerate all the members in Domain Admin

We are going to enumerate all the members that are part of the Domain Admins group.

Command

AdFind -b "CN=Domain Admins,CN=Users,DC=contoso,DC=com" member

Result

At the sample results, we can see all the Domain Admins being returned.

  • Enumerate all accounts with adminCount set to 1

We are now going to run a LDAP query that looks for accounts being part of at least one protected group.

Command

AdFind.exe -default -f "(&(adminCount=1)(objectClass=user))" -dn

Result

At the sample result, we can see all the accounts that are part of at least one protected group.

  • Enumerate all servers configured for Unconstrained Delegation (Excluding DCs)

A server that is trusted for unconstrained delegation is allowed to impersonate (almost) any user to any service within the network. These kind of servers are highly valuable from an attackers point of view, because compromising one of such servers lead to privilege escalation to DA.

Command

AdFind.exe -default -f "(&(objectCategory=computer)(!(primaryGroupID=516)(userAccountControl:1.2.840.113556.1.4.803:=524288)))" dnsHostName OperatingSystem lastlogonTimestamp pwdLastSet

Result

At the sample result, we can see there is one server configured for Unconstrained Delegation.

  • Enumerating accounts with SPN

User accounts with SPN provide attackers the chance to request a Kerberos TGS of it and crack the ticket offline to obtain the password of the targeted account. This command will discover all the accounts that have a SPN.

Command

AdFind.exe -default -f "(&(objectCategory=user)(servicePrincipalName=*))" cn serviceprincipalname pwdlastset lastlogontimestamp

Result

At the sample result, we can see that there a couple of accounts with a SPN.

  • Enumerating accounts being trusted for Unconstrained Delegation

First, we were looking for computers that are configured for Unconstrained Delegation. Now we are going to look for accounts being configured for Unconstrained Delegation

Command

AdFind.exe -default -f "(&(objectCategory=user)(userAccountControl:1.2.840.113556.1.4.803:=524288))"

Result

At the sample result, we can see that 0 accounts have been configured for Unconstrained Delegation.

  • Enumerating DACL Permissions on the AdminSDHolder container

Permissions can be delegated on the AdminSDHolder container, which is something terrible to do by the way. An ACE with GenericWrite or equivalent on the AdminSDHolder container is basically just a DA. This command will look for all the permissions that have been delegated on the container.

Command

AdFind -b "CN=AdminSDHolder,CN=System,DC=contoso,DC=com" -s base nTSecurityDescriptor -sddl++ -resolvesids

Result

At the sample result, we can see the permissions that have been delegated on the AdminSDHolder container.

  • Enumerating DACL on the Domain Root Object

We are now going to look for all the permissions that have been delegated on the Domain Root Object.

AdFind -b "DC=contoso,DC=com" -s base nTSecurityDescriptor -sddl++ -resolvesids

Result

At the sample result, we can see all the permissions that have been delegated on the domain root object.

  • Enumerating LAPS password

Local Administrator Password Solution (LAPS) randomizes the local admin password of the RID-500 account on every machine that has LAPS installed on. Their is a special LDAP attribute with the name “ms-Mcs-AdmPwd” that stores the LAPS password, but only the right people that have the permission can view the attribute.

Command

AdFind.exe -default -f "(&(objectCategory=computer)(ms-MCS-AdmPwd=*))" dnsHostName ms-Mcs-AdmPwd

Result

At the sample result, we can see a few machines including the LAPS password.

  • Enumerating accounts with Kerberos Pre-Authentication disabled

Once pre-authentication is disabled, an attacker could request authentication data for any user and a Domain Controller would return an encrypted TGT that can be brute-forced offline. This command will look for accounts that have Kerberos Pre-Authentication disabled.

Command

AdFind.exe -default -f "(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=4194304))" cn useraccountcontrol

Result

At the sample result, we can see that two accounts have this specific setting configured.

  • Enumerating all domain trusts

This command will look for all the domain trusts that have been established.

Command

AdFind.exe -default -f "(&(objectClass=trustedDomain))"

Result

At the sample result, we can see that there are no domain trusts.

  • Enumerating subnets

This command will look at all the subnets that are stored within AD.

Command

AdFind -subnets -f (objectCategory=subnet) name

Result

At the sample result, we can see the subnets being listed.

OpSec mistakes during LDAP reconnaissance

In this section, we are going to demonstrate an example on how attackers can ring bells when they are not careful with doing their LDAP reconnaissance.

We will use SharpHound to demonstrate this.

First, we are going to collect various data with SharpHound.

Command

Invoke-BloodHound -Loop -LoopInterval 00:01:00 -LoopDuration 00:10:00

Result

At the sample result, we can see that we have triggered 3 different alerts in Defender for Endpoint. Let’s break it down to understand why Defender for Endpoint trigger such kind of alerts.

  • Suspicious LDAP query

If we look at the “Suspicious LDAP query” alert, we can see that the following LDAP query was ran:

(|(|(samaccounttype=268435456)(samaccounttype=268435457)(samaccounttype=536870912)(samaccounttype=536870913)(primarygroupid=*))(&(sAMAccountType=805306369)(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))(|(samAccountType=805306368)(samAccountType=805306369)(samAccountType=268435456)(samAccountType=268435457)(samAccountType=536870912)(samAccountType=536870913)(objectClass=domain)(&(objectcategory=groupPolicyContainer)(flags=*))(objectcategory=organizationalUnit))(objectclass=domain)(|(samaccounttype=268435456)(samaccounttype=268435457)(samaccounttype=536870912)(samaccounttype=536870913)(samaccounttype=805306368)(samaccounttype=805306369)(objectclass=domain)(objectclass=organizationalUnit)(&(objectcategory=groupPolicyContainer)(flags=*)))(|(&(&(objectcategory=groupPolicyContainer)(flags=*))(name=*)(gpcfilesyspath=*))(objectcategory=organizationalUnit)(objectClass=domain))(&(samaccounttype=805306368)(serviceprincipalname=*)))

At the beginning of this blog post, we discussed that it is very rarely that you will see a process running a LDAP query that contains multiple search filters in one request. This already indicates, that there’s a high chance of an attacker doing reconnaissance.

  • Outbound connection to non-standard port

We have also triggered an alert with the title “Outbound connection to non-standard port”. In the screenshot down below, we can see that PowerShell made an outbound connection from 10.0.0.5 to 10.0.0.9:3268

Port 3268 is used for queries specifically targeted for the Global Catalog. LDAP requests sent to port 3268 can be used to search for objects in the entire forest.

The Global Catalog is a feature of Active Directory domain controllers that allows for a domain controller to provide information on any object in the forest, regardless of whether the object is a member of the domain controller’s domain.

  • Summary

If we now just summarize all the alerts that were triggered. First, we had an LDAP query with multiple search filters, looking for all the users, computers, groups, GPOs, domains, SPNs, etc. Second, the LDAP request was sent to a Global Catalog server, which is a server in a forest that’s capable of providing all the information of every object in the entire forest, regardless of whether the object is a member of the Domain Controller’s domain. All of this looks indeed to an attacker performing reconnaissance.

How to make less noise during LDAP reconnaissance?

In this section, we are going to share some tips to make less noise when doing LDAP reconnaissance.

We are going to use SharpHound again, but during this time. We will exclude Domain Controllers, so we will not be able to collect anything specified in the DCOnly collection method.

Command

Invoke-BloodHound -Loop -LoopInterval 00:01:00 -LoopDuration 00:10:00 -ExcludeDomainControllers

Result

We will now only receive one alert, which is that we invoked a malicious cmdlet. This is a signature based rule, but we won’t trigger any alerts related to the likes of “Suspicious LDAP query” for example. Customizing SharpHound is not the hardest part though. However, that’s not the goal of this blog post for today.

OpSec tips

  • Avoid sending LDAP queries against a Global Catalog server
  • Try to avoid running LDAP queries with multiple search filters like SharpHound does
  • Try avoiding querying the Domain Admins group
  • We can still do SPN & LAPS recon, querying less protected groups like DnsAdmins and Exchange groups, discovering domain trusts, subnets, DNS records, enumerating ACLs on the Domain Root Object, etc.

LDAP Reconnaissance against Microsoft’s Defender for Identity

Defender for Identity is a cloud-based security solution that leverages On-Premises Active Directory signals to identify and detect threats. It monitors Domain Controllers by capturing it’s network traffic to leverage it with Windows event logs to analyze data for attacks, that might occur on a network. It contains a sensor that needs to be installed on the Domain Controllers.

In this section, we are going to cover LDAP queries that will trigger alerts in Defender for Identity. This allows attackers to be more careful when doing recon.

Since not everybody has MDI in their lab environment, but they perhaps do have it in their production environment. However, they can’t install SharpHound & ADFind, because Windows Defender AV will block it… Well, because of that. We will use the ADSI accelerator in PowerShell to run our LDAP queries, which basically achieves the same as ADFind.

  • Eumerate all servers configured for Unconstrained Delegation (Excluding DCs)

Command

([adsisearcher]'(&(objectCategory=computer)(!(primaryGroupID=516)(userAccountControl
:1.2.840.113556.1.4.803:=524288)))').FindAll()

Result

At the sample result, we can see that an alert has been triggered in MDI. After we were looking for servers configured for Unconstrained Delegation through the LDAP protocol.

  • Enumerate accounts with Kerberos Pre-Authentication disabled

Command

([adsisearcher]'(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=4194304))').FindAll()
  • Enumerate accounts with Kerberos DES enabled

Command

([adsisearcher]'(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=2097152))').FindAll()

Result

At the sample result, we can see that MDI has triggered an alert on this. This will work for all the different types of user account control values being configured on an account, so this is something to avoid.

  • Enumerate all enabled accounts

Command

([adsisearcher]'(&(objectCategory=person)(objectClass=user)(!(userAccountControl
:1.2.840.113556.1.4.803:=2)))').FindAll()
  • Enumerate all global security groups
([adsisearcher]'(groupType=-2147483646)').FindAll()

Result

At the sample result, we can see that MDI triggers an alert. Once we run a LDAP query to gather all the user accounts and global security groups. This alert will trigger as well, when you query a specific OU and request all the computers that resides in that OU to return back at results.

  • Enumerate all the computers

Command

([adsisearcher]'(objectCategory=computer)').FindAll()

Result

At the sample result, we can see that MDI has triggered an alert for it.

  • Enumerate sensitive AD groups

Command

([adsisearcher]'(&(adminCount=1)(objectClass=group))').FindAll()

Result

At the sample result, we can see that MDI triggers an alert. Once we were querying builtin groups in AD that are considered sensitive.

  • Enumerate servers configured for Resource Based Constrained Delegation

Command

repadmin /showattr * DC=contoso,DC=com /subtree /filter:"((&(objectClass=computer)(msDS-AllowedToActOnBehalfOfOtherIdentity=*)))" /attrs:cn,msDs-AllowedToActOnBehalfOfOtherIdentit

Result

At the sample result, we can see that MDI has triggered an alert after we were querying for that special AD attribute (msDS-AllowedToActOnBehalfOfOtherIdentity).

Advanced Hunting – LDAP Search Filters

The final part of this blog post will be all about Advanced Hunting. We have ran multiple LDAP queries by using tools like ADFind and SharpHound. Defender for Endpoint & Identity do have built-in detection’s in place, but it is impossible to expect, that it would detect every LDAP query. All of these queries have been tested in a production environment to see if there is noise, but please re-test. Since every organization is different from each other.

  • Hunting multiple LDAP queries ran in a short period of time

First example will cover the behavior behind SharpHound. Once we ran SharpHound, it will collect various of data. We know that once we exclude the Domain Controllers. It will be less noisy, and Defender for Endpoint may not flag it, so that is a great use-case to use Advanced Hunting.

What SharpHound does is, that it will run multiple LDAP queries to discover in which OU all the users and computers are stored. It will then query the relevant attributes to determine whether it is a user or machine account, etc. In order to do this, it will run a LDAP query that looks similar to this:

(|(samAccountType=805306368)(samAccountType=805306369)(objectclass=organizationalUnit))

Query

Our query will look if an actor has ran 10 LDAP queries in just one minute, but we have specified that we are only interested in LDAP search filters that looks for users and computers within AD.

let timeframe = 7d;
let Thershold = 10; 
let BinTime = 1m;
DeviceEvents
| where Timestamp >= ago(timeframe)
| where ActionType == "LdapSearch"
| extend LDAP = parse_json(AdditionalFields)
| extend SearchFilter = LDAP.SearchFilter
| where SearchFilter has "samAccountType=805306368" // LDAP filter to discover all the user accounts
    or SearchFilter has "samAccountType=805306369" // LDAP filter to discover all the machine accounts
| summarize NumberOfLdapQueries = count() by tostring(SearchFilter), bin(Timestamp, BinTime), DeviceName
| where NumberOfLdapQueries > Thershold
| project-reorder Timestamp, DeviceName, SearchFilter, NumberOfLdapQueries

Result

At the sample result, we can see that two machines have ran multiple LDAP queries in a very short period to look for all the users and computers.

  • Service Principal Reconnaissance

An adversary can look for all the user accounts that have a SPN configured. This allows an adversary to request a Kerberos TGS of the targeted account and export that TGS offline to brute-force the password offline.

An adversary can run a LDAP search filter with a wildcard at the servicePrincipalName attribute to gather all accounts with a SPN.

((&(objectCategory=user)(servicePrincipalName=*)))

Query

Our query will look when an actor is running a LDAP query that has a wildcard to discover all the user accounts with a SPN.

let timeframe = 7d;
let ldap_filter = dynamic(["servicePrincipalName=*"]); // Wildcard search to find user accounts with a SPN
DeviceEvents
| where Timestamp >= ago(timeframe)
| where ActionType == 'LdapSearch'
| extend LDAP = parse_json(AdditionalFields)
| extend AttributeList = LDAP.AttributeList
| extend SearchFilter = LDAP.SearchFilter
| extend DistinguishName = LDAP.DistinguishedName
| where SearchFilter has_any (ldap_filter)
| project Timestamp, DeviceName, SearchFilter, DistinguishName, AttributeList

Result

At the sample result, we can see LDAP search filters that contains a wildcard search looking for accounts with a service principal name.

  • Querying LAPS password

Once LAPS is rolled out on all the machines. The LAPS password is stored in a special attribute “ms-Mcs-AdmPwd” on a machine account in Active Directory. An actor with the right permissions is able to query the LAPS password on a machine to obtain the password of the RID-500 account.

Query

Our query will look if someone has queried the LAPS password. We will exclude all the SHA1 hashes of admpwd.ui.exe in our query, because it is the LAPS GUI. Other processes can be excluded as well to reduce the noise of the query.

let allLAPSAdminHashes = DeviceProcessEvents
| where FileName =~ "admpwd.ui.exe" // Excluding admpwd.ui.exe, because it is the LAPS GUI
| summarize by SHA1
| invoke FileProfile(SHA1, 100)
| where IsRootSignerMicrosoft and IsCertificateValid
| project SHA1;
let ldap_filter = dynamic(["ms-MCS-AdmPwd"]); 
DeviceEvents
| where ActionType == "LdapSearch"
| where InitiatingProcessAccountName != "network service"
| where InitiatingProcessSHA1 !in (allLAPSAdminHashes)
| extend LDAP = parse_json(AdditionalFields)
| extend AttributeList = LDAP.AttributeList
| extend SearchFilter = LDAP.SearchFilter
| extend DistinguishName = LDAP.DistinguishedName
| where SearchFilter has_any (ldap_filter)
| project Timestamp, DeviceName, InitiatingProcessAccountName, InitiatingProcessFileName, AttributeList, SearchFilter, DistinguishName
| sort by Timestamp desc

Result

At the sample result, we can see that an actor was using ADFind to query the LAPS password.

  • ACL Permission Discovery on Domain Root Object

An actor may query the Domain Root Object to gather all the permissions that have been delegated. ACEs with GenericAll or equivalent permission have the rights to do DCSync to obtain all the NT hashes of all the accounts within the domain. The nTSecurityDescriptor attribute contains the access permissions for the AD object.

Query

Our query will look whether an actor has queried for the DACL permissions being set on the Domain Root Object. We have excluded management tools with the likes of mmc.exe to avoid the false-positives.

let Hashes = DeviceProcessEvents
| where InitiatingProcessFileName =~ "mmc.exe"
    or InitiatingProcessFileName =~ "microsoft.activedirectory.webservices.exe"
| summarize by InitiatingProcessSHA1
| invoke FileProfile(InitiatingProcessSHA1, 100)
| where IsRootSignerMicrosoft and IsCertificateValid
| project SHA1;
DeviceEvents
| where ActionType == "LdapSearch"
| where InitiatingProcessSHA1 !in (Hashes)
| extend LDAP = parse_json(AdditionalFields)
| extend AttributeList = LDAP.AttributeList
| extend SearchFilter = LDAP.SearchFilter
| extend DistinguishName = LDAP.DistinguishedName
| where AttributeList has "ntSecurityDescriptor"
| where DistinguishName == "DC=contoso,DC=com" // Replace contoso with your domain name
| project Timestamp, DeviceName, InitiatingProcessAccountName, InitiatingProcessFileName, AttributeList, SearchFilter, DistinguishName
| sort by Timestamp desc

Result

At the sample result, we can see that two users were using ADFind to query the DACL permissions on the Domain Root Object.

  • AD Attributes

An actor may query for special attributes such as “msDS-AllowedToDelegateTo” to find servers being configured for Constrained Delegation. Second attribute can be the “msDSMachineAccountQuota” to find how many machine accounts an authenticated user can create.

Query

let timeframe = 7d;
let attr_list = dynamic(["msDs-AllowedToDelegateTo","ms-DS-MachineAccountQuota"]);
DeviceEvents
| where Timestamp >= ago(timeframe)
| where ActionType == 'LdapSearch'
| where InitiatingProcessAccountName != "local service"
| extend LDAP = parse_json(AdditionalFields)
| extend AttributeList = LDAP.AttributeList
| extend SearchFilter = LDAP.SearchFilter
| extend DistinguishName = LDAP.DistinguishedName
| where AttributeList has_any (attr_list)
| project Timestamp, InitiatingProcessAccountName, DeviceName, SearchFilter, DistinguishName, AttributeList
| sort by Timestamp desc

Result

At the sample result, we can see that an actor has queried the specified AD attributes.

Reference

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s