Skip to content

add ldap3 Kerberos authentication function#843

Closed
azoxlpf wants to merge 5 commits intoPennyw0rth:mainfrom
azoxlpf:feat/ldap3-kerberos-auth
Closed

add ldap3 Kerberos authentication function#843
azoxlpf wants to merge 5 commits intoPennyw0rth:mainfrom
azoxlpf:feat/ldap3-kerberos-auth

Conversation

@azoxlpf
Copy link
Copy Markdown
Contributor

@azoxlpf azoxlpf commented Aug 5, 2025

Description

This PR introduces a pure-Python LDAP bind routine that uses the ldap3 library with SPNEGO / Kerberos authentication (ldap3_kerberos_login). Until now NetExec bound over LDAP exclusively with Impacket’s low-level
LDAPConnection, which:

  • accepts every secret type (password, NT/LM hash, AES key, ccache),
  • but does not expose high-level CRUD helpers (modify(), add(), delete(), …).

ldap3, on the other hand, offers a complete Pythonic API for directory operations but lacked a Kerberos path inside NetExec. The new function bridges that gap:

  • requests (or re-uses) a TGT/TGS, wraps it in a SPNEGO blob and completes
    a SASL bind through ldap3,
  • normalises both TGT and TGS into a common dict format
    ({"KDC_REP": blob, "cipher": …, "sessionKey": …}) so downstream code can
    consume them without branching.

Why ldap3 matters

  • Enables attribute manipulations required by the upcoming
    --targetedkerberoast PR (temporary add/remove of servicePrincipalName) Add targeted Kerberoasting by injecting/removing temporary SPNs
  • Provides an API that future features (delegation abuse, ACL edits, etc.)
    can reuse without re-implementing ASN.1.
  • Keeps everything pure Python – no additional binaries or
    system libraries beyond the already-present ldap3.

Dependencies

  • No new external dependencies.
    ldap3, impacket, pyasn1 and Cryptodome are already shipped with NetExec.

Summary

Add a fully-featured ldap3 Kerberos bind, paving the way for
SPN-injection workflows (Targeted Kerberoast) and any future PR that needs
to modify LDAP objects while remaining compatible with all credential types.

Type of change

Insert an "x" inside the brackets for relevant items (do not delete options)

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Deprecation of feature or functionality
  • This change requires a documentation update
  • This requires a third party update (such as Impacket, Dploot, lsassy, etc)

Setup guide for the review

Component Version / Build
Exegol (free) 3.16
Host OS Ubuntu 22.04.5 LTS
Python 3.11 (system default inside Exegol 3.16)
Target DC Windows Server 2022 (build 20348) – AD domain AZOX.HACKLAB
Test workstation Windows 10 21H2 (joined to the same domain)

No additional packages were installed; everything runs with the libraries already shipped in NetExec (ldap3, impacket, pyasn1, Cryptodome).

Screenshots (if appropriate):

The capture shows the --test helper (a thin wrapper that simply calls ldap3_kerberos_login) succeeding with a clear-text password, an NT hash, AES-256 and AES-128 keys, and finally with a ticket pulled from the local ccache :

ldap3

Checklist:

Insert an "x" inside the brackets for completed and relevant items (do not delete options)

  • I have ran Ruff against my changes (via poetry: poetry run python -m ruff check . --preview, use --fix to automatically fix what it can)
  • I have added or updated the tests/e2e_commands.txt file if necessary (new modules or features are required to be added to the e2e tests)
  • New and existing e2e tests pass locally with my changes
  • If reliant on changes of third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)

@NeffIsBack
Copy link
Copy Markdown
Member

Hi, first of all thanks for your work!

We have discussed switching to ldap3 instead of impackets ldap implementation already internally a while ago and decided to not switch for several reasons:

  • iirc ldap3 does not support encryption/signing without external, non-python packages
  • NetExec is build on the impacket implementation, switching to ldap3 would require massive changes
  • The dev team and the community knows impacket much better by now compared to ldap3 which makes it harder to debug errors (not the strongest argument, but definitely doesn't make it easier to switch)

Imo if we should need the TGT/TGS of the connection we should implement getter/setter methods in impacket to retrieve them from the connection.

The missing CRUD methods are indeed very annoying, but i think before we implement and establish a different kerberos login method which enables you to do other things than with the standard kerberos login we should try to implement these methods in impacket. Otherwise we will run into situations where we have a different ldap connection than assumed and stuff will fall apart due to missing functions.

In the meantime i would go for the same approach as other modules do that need the CRUD methods: Use the existing credentials in NetExec to establish a new ldap3 connection inside that module.

That are at least my thoughts :)

@azoxlpf
Copy link
Copy Markdown
Contributor Author

azoxlpf commented Aug 5, 2025

Hi, first of all thanks for your work!

We have discussed switching to ldap3 instead of impackets ldap implementation already internally a while ago and decided to not switch for several reasons:

iirc ldap3 does not support encryption/signing without external, non-python packages
NetExec is build on the impacket implementation, switching to ldap3 would require massive changes
The dev team and the community knows impacket much better by now compared to ldap3 which makes it harder to debug errors (not the strongest argument, but definitely doesn't make it easier to switch)
Imo if we should need the TGT/TGS of the connection we should implement getter/setter methods in impacket to retrieve them from the connection.

The missing CRUD methods are indeed very annoying, but i think before we implement and establish a different kerberos login method which enables you to do other things than with the standard kerberos login we should try to implement these methods in impacket. Otherwise we will run into situations where we have a different ldap connection than assumed and stuff will fall apart due to missing functions.

In the meantime i would go for the same approach as other modules do that need the CRUD methods: Use the existing credentials in NetExec to establish a new ldap3 connection inside that module.

Thanks for the detailed feedback!

Just to clarify: this PR does not aim to replace Impacket’s LDAP implementation or change how NetExec handles LDAP globally. It simply introduces a utility function (ldap3_kerberos_login) that creates a ldap3 connection when needed — for example, in upcoming features like --targetedkerberoast that require LDAP operations such as modify(), add(), etc.

I completely understand the concerns about moving away from Impacket — but this PR is not proposing such a migration. If it helps, I’m happy to rename or further isolate the function to make it clear that it’s a helper, meant to be used only when needed, without affecting the core behavior.

@NeffIsBack
Copy link
Copy Markdown
Member

Hi, first of all thanks for your work!
We have discussed switching to ldap3 instead of impackets ldap implementation already internally a while ago and decided to not switch for several reasons:
iirc ldap3 does not support encryption/signing without external, non-python packages
NetExec is build on the impacket implementation, switching to ldap3 would require massive changes
The dev team and the community knows impacket much better by now compared to ldap3 which makes it harder to debug errors (not the strongest argument, but definitely doesn't make it easier to switch)
Imo if we should need the TGT/TGS of the connection we should implement getter/setter methods in impacket to retrieve them from the connection.
The missing CRUD methods are indeed very annoying, but i think before we implement and establish a different kerberos login method which enables you to do other things than with the standard kerberos login we should try to implement these methods in impacket. Otherwise we will run into situations where we have a different ldap connection than assumed and stuff will fall apart due to missing functions.
In the meantime i would go for the same approach as other modules do that need the CRUD methods: Use the existing credentials in NetExec to establish a new ldap3 connection inside that module.

Thanks for the detailed feedback!

Just to clarify: this PR does not aim to replace Impacket’s LDAP implementation or change how NetExec handles LDAP globally. It simply introduces a utility function (ldap3_kerberos_login) that creates a ldap3 connection when needed — for example, in upcoming features like --targetedkerberoast that require LDAP operations such as modify(), add(), etc.

I completely understand the concerns about moving away from Impacket — but this PR is not proposing such a migration. If it helps, I’m happy to rename or further isolate the function to make it clear that it’s a helper, meant to be used only when needed, without affecting the core behavior.

Okay got it! I think until we have CRUD methods in impackets LDAP version it is fine to provide a method to establish a ldap3 LDAP connection which can be used without having to invest time into implementing the ldap3 bind&auth part.

I think we should indeed rename it to something like get_ldap3_connection or something similar, which automatically picks up the supplied credential set and arguments with which we have established the impacket ldap connection. Basically self.get_ldap3_connection() and this internally checks if there are nt/lm/aes hashes or if we use kerberos/ntlm/ccache etc. However, i think we should also respect the --kerberos flag and only authenticate via tickets if netexec is told to.

Furthermore, does this handshake require the gssapi package of linux? Does this work with native python code as you have implemented the TGT/TGS flow manually? Otherwise we might run into problems when running on other platforms such as Windows or MacOS.

@azoxlpf
Copy link
Copy Markdown
Contributor Author

azoxlpf commented Aug 7, 2025

Hi, first of all thanks for your work!
We have discussed switching to ldap3 instead of impackets ldap implementation already internally a while ago and decided to not switch for several reasons:
iirc ldap3 does not support encryption/signing without external, non-python packages
NetExec is build on the impacket implementation, switching to ldap3 would require massive changes
The dev team and the community knows impacket much better by now compared to ldap3 which makes it harder to debug errors (not the strongest argument, but definitely doesn't make it easier to switch)
Imo if we should need the TGT/TGS of the connection we should implement getter/setter methods in impacket to retrieve them from the connection.
The missing CRUD methods are indeed very annoying, but i think before we implement and establish a different kerberos login method which enables you to do other things than with the standard kerberos login we should try to implement these methods in impacket. Otherwise we will run into situations where we have a different ldap connection than assumed and stuff will fall apart due to missing functions.
In the meantime i would go for the same approach as other modules do that need the CRUD methods: Use the existing credentials in NetExec to establish a new ldap3 connection inside that module.

Thanks for the detailed feedback!
Just to clarify: this PR does not aim to replace Impacket’s LDAP implementation or change how NetExec handles LDAP globally. It simply introduces a utility function (ldap3_kerberos_login) that creates a ldap3 connection when needed — for example, in upcoming features like --targetedkerberoast that require LDAP operations such as modify(), add(), etc.
I completely understand the concerns about moving away from Impacket — but this PR is not proposing such a migration. If it helps, I’m happy to rename or further isolate the function to make it clear that it’s a helper, meant to be used only when needed, without affecting the core behavior.

Okay got it! I think until we have CRUD methods in impackets LDAP version it is fine to provide a method to establish a ldap3 LDAP connection which can be used without having to invest time into implementing the ldap3 bind&auth part.

I think we should indeed rename it to something like get_ldap3_connection or something similar, which automatically picks up the supplied credential set and arguments with which we have established the impacket ldap connection. Basically self.get_ldap3_connection() and this internally checks if there are nt/lm/aes hashes or if we use kerberos/ntlm/ccache etc. However, i think we should also respect the --kerberos flag and only authenticate via tickets if netexec is told to.

Furthermore, does this handshake require the gssapi package of linux? Does this work with native python code as you have implemented the TGT/TGS flow manually? Otherwise we might run into problems when running on other platforms such as Windows or MacOS.

Got it ! I’ll rename it to get_ldap3_connection() and make it automatically reuse the credentials/args from the Impacket LDAP connection, respecting --kerberos exactly like the classic flow.

It works with both NTLM and Kerberos (TGT/TGS built in pure Python), and does not depend on the system’s gssapi package, I tested successfully on Linux without it, and on Windows. I don’t have a Mac to test, but it should work as it’s platform-independent.

Windows AES :

windows_aes

Windows kcache :

windows_kcache

@Marshall-Hallenbeck
Copy link
Copy Markdown
Collaborator

I would name it create_ldap3_connection, not "get", since that could mean it already exists and you are asking to retrieve it.

@azoxlpf
Copy link
Copy Markdown
Contributor Author

azoxlpf commented Aug 7, 2025

I’m fine with either naming. It’s up to you.

@azoxlpf
Copy link
Copy Markdown
Contributor Author

azoxlpf commented Aug 8, 2025

I’ve updated the PR according to the feedback:

  • Function renamed to create_ldap3_connection as suggested.

  • It now automatically reuses the credentials and arguments from the existing Impacket LDAP connection.

  • Fully respects the --kerberos flag and authentication flow (NTLM/Kerberos/ccache detection).

  • Supports both NTLM and Kerberos in pure Python (no gssapi dependency), tested successfully on Linux and Windows.

  • Works with both LDAP (389) and LDAPS (636), automatically choosing/fallback based on --port and server requirements.

Let me know if anything else should be adjusted.

@mpgn
Copy link
Copy Markdown
Collaborator

mpgn commented Aug 8, 2025

Interesting pr :)

@NeffIsBack NeffIsBack added the enhancement New feature or request label Aug 10, 2025
Comment thread nxc/protocols/ldap.py Outdated
@NeffIsBack NeffIsBack mentioned this pull request Oct 27, 2025
13 tasks
Copy link
Copy Markdown
Member

@NeffIsBack NeffIsBack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without going too much indepth, here is a first review.

So, in order to ensure that modules if e.g. chained can call this function as often as needed we should:

  • Move the main logic into nxc/protocols/ldap/ldap3_conn.py (or something similarly called, but separate file)
  • Create a class that is a wrapper for the object instantiation
  • Implement a singleton pattern for the instantiation: if self.ldap3_conn: return self.ldap3_conn, else: self.ldap3_conn = LDAP3_CONN(args).create_conn(), return self.ldap3_conn
  • Rename function in ldap.py back to get_ldap3_connection to emphasize calling this function as often as needed

After that we should replace all manual creations of ldap3 connections in modules. Vice versa, when impacket.ldap finally has an implementation for CRUD functions we can rip out this workaround and replace it with impacket.ldap.

Comment thread nxc/protocols/ldap.py Outdated
"""
Return an ldap3.Connection.

- NTLM bind requires a plaintext password.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there no pass-the-hash implementation in ldap3?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there is pass-the-hash support in ldap3 with NTLM. I think I initially misread the ldap3 documentation when I worked on that PR

Comment thread nxc/protocols/ldap.py Outdated
Comment on lines +506 to +509
hash_only = (not self.password) and (bool(self.nthash) or bool(self.lmhash))
aes_only = (not self.password) and bool(self.aesKey)
use_k = self.kerberos or hash_only or aes_only
use_cc = getattr(self, "use_kcache", False)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should only be either password, hash, aesKey or kcache. A combination is not possible, so there should not be a use case for "only" variables. Take a look at the connection.py for the logic of netexec authentication.

Comment thread nxc/protocols/ldap.py Outdated
if hash_only and not self.kerberos:
self.logger.display("No -k supplied: switching to Kerberos because ldap3 NTLM doesn’t support hash-only auth.")

if not use_k:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defining functions depending on states is dangerous. The check should be when the function is used and not on definition.

Comment thread nxc/protocols/ldap.py Outdated
self.logger.fail(f"NTLM bind failed: {last_err}")
return None

self.ldap_connection = conn
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This attribute should never be overwritten. Instead, create another variable (e.g. self.ldap3_connection) which can then be used by modules/functions which explicitely need to use ldap3 instead of impacket.

Comment thread nxc/protocols/ldap.py Outdated
Comment on lines +562 to +564
self.logger.info(
f"ldap3 NTLM bind over {'LDAPS:636' if conn.server.port == 636 else f'LDAP:{conn.server.port}'} established"
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't scatter function calls like that. That just wastes space and decreases readability.

@azoxlpf
Copy link
Copy Markdown
Contributor Author

azoxlpf commented Dec 13, 2025

Without going too much indepth, here is a first review.

So, in order to ensure that modules if e.g. chained can call this function as often as needed we should:

  • Move the main logic into nxc/protocols/ldap/ldap3_conn.py (or something similarly called, but separate file)
  • Create a class that is a wrapper for the object instantiation
  • Implement a singleton pattern for the instantiation: if self.ldap3_conn: return self.ldap3_conn, else: self.ldap3_conn = LDAP3_CONN(args).create_conn(), return self.ldap3_conn
  • Rename function in ldap.py back to get_ldap3_connection to emphasize calling this function as often as needed

After that we should replace all manual creations of ldap3 connections in modules. Vice versa, when impacket.ldap finally has an implementation for CRUD functions we can rip out this workaround and replace it with impacket.ldap.

I’ve implemented the suggested changes:

  • Created nxc/protocols/ldap/ldap3_conn.py with a Ldap3Connection wrapper class

  • Implemented a singleton pattern in get_ldap3_connection() to allow safe reuse across chained modules

  • Renamed the function back to get_ldap3_connection() to emphasize that it can be called multiple times

  • Added pass-the-hash support for NTLM authentication

I also took the opportunity to refactor the code to improve overall readability

@azoxlpf azoxlpf closed this Dec 22, 2025
@azoxlpf
Copy link
Copy Markdown
Contributor Author

azoxlpf commented Dec 22, 2025

I’ll close it since @NeffIsBack implemented CRUD support in Impacket Add CRUD methods to ldap, it’ll be much easier to use that way

@azoxlpf azoxlpf deleted the feat/ldap3-kerberos-auth branch December 23, 2025 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants