BlueTeam, LDAP, Nmap, RedTeam, Windows

Searching LDAP using Nmap’s ldap-search.nse script

Nmap has an NSE script, ldap-search.nse, that enables performing queries against LDAP ( Lightweight Directory Access Protocol) services. The goal of this post is to provide an introduction to using the script as well as a couple of practical examples.

Before you get started

I strongly urge you to use the latest version of Nmap which is currently 7.50.  If you wish for all LDAP attributes to be returned you may need to use the version of Nmap from the Subversion code repository [2]  as it contains a fix [3] for a bug that caused the ldap-search script to crash when handling Active Directory’s objectSID attribute. Official Nmap releases after 7.50 will contain this fix.

Additionally, in the examples below I use the standard LDAP port of 389/tcp.  If your target has implemented LDAP over SSL (LDAPS) I strongly recommend that you use port 636/tcp instead so that all requests are encrypted using TLS. This is a non-default configuration for Active Directory and requires that a certificate be install on the target. These commands should work against LDAPS even if the target’s IP address hasn’t been (using -n Nmap option) or can’t be resolved to a host name.

Before you get started you may wish to glance over the ldap-search NSEDOC page. This could be useful but isn’t required.  If you have issues with Nmap’s –script-args option you can read more about it in the Nmap online book.

If anything here isn’t clear please let me know by posting a comment or tossing a message to @TomSellers on Twitter.

Basic usage

Let’s take a look at the minimum requirements for the script by looking at the example below.  I’m using *nix line continuation to make it more readable.  On Windows you will need to remove the backslashes at the end of each line and collapse this down to one line.

sudo nmap -p 389 --script ldap-search \
--script-args \
'ldap.username="CN=Administrator,CN=Users,DC=adlab,DC=pwnable", \
ldap.password="AdminPasswordHere", \
ldap.qfilter=computers' \
192.168.50.231

In order to use ldap-search we need to provide credentials with rights to access LDAP on the target. We specify these using ldap.username and ldap.password in the script-args portion of the command. Note that you can wrap the values in script-args in single or double quotes but that you need to pay close attention to how they are nested as this can cause problems if not handled properly. In my examples I’ll use single quotes to wrap script-args and double quotes to wrap any values with it such as ldap.username and ldap.password.

The next script argument in script-args is ldap.qfilter and specifies a quick filter. A quick filter, in this context, can be thought of a “canned” LDAP search string. These are needed as the script doesn’t truly support the full LDAP syntax. In the command above this argument isn’t technically required since it will default to the value of all but it’s important to understand the request that you are sending.

  • all – Returns all objects
  • ad_dcs – Returns only domain controllers
  • computers -Returns only computer objects
  • users – Returns objects where the objectClass matches ‘user’, ‘posixAccount’, or ‘person’
  • custom – This allows the user to specify an attribute and value to search by. I’ll cover this as part of a practical application section.

By default ldap-search will only return 20 results.  This can be changed using the ldap.maxobjects script argument.  Setting this to -1 removes the limit completely.

 

Practical application – saving selected information about all computers to a CSV

In this example we’re going to explore some real world usage. Take at look at the command below.

sudo nmap -p 389 --script ldap-search \
--script-args \
'ldap.username="CN=Administrator,CN=Users,DC=adlab,DC=pwnable", \
ldap.password="AdminPasswordHere", \
ldap.qfilter=computers,
ldap.attrib={name,dNSHostName,operatingSystem}, \
ldap.savesearch=test' \
192.168.50.231

This command returns the ‘name’, ‘dNSHostName’, and ‘operatingSystem’ attributes for all computers, displays it to the screen, and saves it to a file.

For the value of ldap.qfilter we’re using the value ‘computers’ to return only computer objects as discussed in the previous section. We want to return only specific LDAP attributes so we’re passing the attribute names to ldap.attrib. Since we’re requesting multiple attributes we need to use Lua table syntax which, for simple tables, is comma separated values enclosed by curly ‘{}’ braces.  If we just wanted one value we don’t need the table syntax, for example:

ldap.attrib=name,

Finally, ldap.savesearch is being used to save the query output to a CSV file in the current directory.  The value of this option, in this case ‘test’, is used as the prefix in the output filename.  The filename is constructed as [prefix]_[ip_address]_[port].csv. In this case it would be test_192.168.50.231_389.csv. If the scan includes multiple hosts that return data then one file per target is created. The displayed output of the command above looks something like this:

389/tcp open ldap
| ldap-search: 
| Context: DC=adlab,DC=pwnable; QFilter: computers; Attributes: name,dNSHostName,operatingSystem
|   dn: CN=PWNWINDC01,OU=Domain Controllers,DC=adlab,DC=pwnable
|       name: PWNWINDC01
|       operatingSystem: Windows Server 2012 R2 Datacenter Evaluation
|       dNSHostName: PWNWINDC01.adlab.pwnable
|   dn: CN=W12R2-SQL01,OU=Servers,DC=adlab,DC=pwnable
|       name: W12R2-SQL01
|       operatingSystem: Windows Server 2012 R2 Datacenter Evaluation
|_      dNSHostName: W12R2-SQL01.adlab.pwnable

The CSV output looks something like this:

"name","name","operatingSystem","dNSHostName"
"CN=W12R2-SQL01,OU=Servers,DC=adlab,DC=pwnable","W12R2-SQL01","Windows Server 2012 R2 Datacenter Evaluation","W12R2-SQL01.adlab.pwnable"
"CN=PWNWINDC01,OU=Domain Controllers,DC=adlab,DC=pwnable","PWNWINDC01","Windows Server 2012 R2 Datacenter Evaluation","PWNWINDC01.adlab.pwnable"

Practical application – extracting LAPS passwords and saving to a CSV

Rob Fuller ( @mubix ) posted a great blog entry on how to use the ldapsearch command line tool to access the plain text passwords that Microsoft’s Local Administrator Password Solution (LAPS) stores in Active Directory.  This can be done with the ldap-search NSE script as well.

sudo nmap -p 389 --script ldap-search \
--script-args \
'ldap.username="CN=Administrator,CN=Users,DC=adlab,DC=pwnable", \
ldap.password="AdminPasswordHere", \
ldap.qfilter=computers, \
ldap.attrib=ms-Mcs-AdmPwd, \
ldap.savesearch=LAPS' \
192.168.50.231

As you can see we’re asking for the value of  the ‘ms-Mcs-AdmPwd’ attribute for all computers in the target Active Directory environment. Here’s the displayed output:

389/tcp open ldap syn-ack ttl 128
| ldap-search: 
| Context: DC=adlab,DC=pwnable; QFilter: computers; Attributes: ms-MCS-AdmPwd
|   dn: CN=PWNWINDC01,OU=Domain Controllers,DC=adlab,DC=pwnable
|   dn: CN=W12R2-SQL01,OU=Servers,DC=adlab,DC=pwnable
|_      ms-Mcs-AdmPwd: 40_Y3Rx1oq3%lw$

And here is what would be saved in LAPS_192.168.50.231_389.csv:

"name","ms-Mcs-AdmPwd"
"CN=W12R2-SQL01,OU=Servers,DC=adlab,DC=pwnable","40_Y3Rx1oq3%lw$"
"CN=PWNWINDC01,OU=Domain Controllers,DC=adlab,DC=pwnable",""

Notice that the CSV output includes a value for ‘name’ which is needed but which we didn’t include in our command.  Also, the Active Directory Controller PWNWINDC01 doesn’t have a value for ms-Mcs-AdmPwd because it isn’t configured to use LAPS and so doesn’t have one.

Practical application – custom searches

As I mentioned previously you can create very simple customized searches via the ‘custom’ quick filter. This filter allows you to use ldap.searchattrib and ldap.searchvalue to specify an LDAP attribute and the value you want it to have. The script will return the attributes specified in ldap.attrib for any objects that match this filter.  For example, the command below returns the ‘name’ and ‘operatingSystem’ values for any object where the ‘name’ value contains *SQL*.

sudo nmap -p 389 --script ldap-search \
--script-args \
'ldap.username="CN=Administrator,CN=Users,DC=adlab,DC=pwnable", \
ldap.password="AdminPasswordHere", \
ldap.qfilter=custom, \
ldap.searchattrib="name", \
ldap.searchvalue="*SQL*", \
ldap.attrib={name,operatingSystem}' \
192.168.50.231

Notice how the output differs from previous output in that it only contains one host which has ‘SQL’  in its name.

PORT STATE SERVICE
389/tcp open ldap
| ldap-search: 
| Context: DC=adlab,DC=pwnable; QFilter: custom; Attributes: name,operatingSystem
|   dn: CN=W12R2-SQL01,OU=Servers,DC=adlab,DC=pwnable
|     name: W12R2-SQL01
|_    operatingSystem: Windows Server 2012 R2 Datacenter Evaluation

To list just the computers running any version of Windows Server you could use the following command which equates to an LDAP filter of  ‘(operatingSystem=Windows*Server*)’.

sudo nmap -p 389 --script ldap-search \
--script-args \
'ldap.username="CN=Administrator,CN=Users,DC=adlab,DC=pwnable", \
ldap.password="AdminPasswordHere", \
ldap.qfilter=custom, \
ldap.searchattrib="operatingSystem", \
ldap.searchvalue="Windows*Server*", \
ldap.attrib=name' \
192.168.50.231

Practical application – limiting results to certain OUs

In certain situations you may wish to limit the results to a certain OU. This can be done using the ldap.base script argument to specify the distinguishedName of the OU you wish to search.

sudo nmap -p 389 --script ldap-search \
--script-args \
'ldap.username="CN=Administrator,CN=Users,DC=adlab,DC=pwnable", \
ldap.password="AdminPasswordHere", \
ldap.qfilter=users, \
ldap.base="OU=Admins,DC=adlab,DC=pwnable", \
ldap.attrib={userPrincipalName, lastLogon,pwdLastSet, memberOf}' \
192.168.50.231

In the example above we’re searching the ‘OU=Admins,DC=adlab,DC=pwnable’ OU and all OUs under it for users (ldap.qfilter=users) and returning the values of the ‘userPrincipalName’, ‘lastLogon’, ‘pwdLastSet’, and ‘memberOf’ attributes.

PORT STATE SERVICE
389/tcp open ldap
| ldap-search: 
| Context: OU=Admins,DC=adlab,DC=pwnable; QFilter: users; Attributes: userPrincipalName,lastLogon,pwdLastSet,memberOf
|   dn: CN=Totes Legit,OU=Admins,DC=adlab,DC=pwnable
|     memberOf: CN=Domain Admins,CN=Users,DC=adlab,DC=pwnable
|     lastLogon: 2017/07/08 21:03:16 UTC
|     pwdLastSet: 2017/07/08 20:14:59 UTC
|_    userPrincipalName: [email protected]

Hopefully you will find this information useful. As I mentioned before please don’t hesitate to reach out to me if something isn’t clear.

Good luck!

– Tom Sellers ( @TomSellers )


References:

  1. Nmap NSE script: ldap-search
  2. Installing Nmap from Subversion
    1. Checking out the code
    2. Building Nmap
  3. Github: PR#938 – [NSE] ldap.lua vs AD objectSID
  4. Room 362: DUMP LAPS PASSWORDS WITH LDAPSEARCH
  5. Nmap online book: script-args option

Leave a comment