Skip to content

add Schannel certificate authentication via --schannel - #1277

Open
azoxlpf wants to merge 15 commits into
Pennyw0rth:mainfrom
azoxlpf:feat/add-schannel
Open

add Schannel certificate authentication via --schannel#1277
azoxlpf wants to merge 15 commits into
Pennyw0rth:mainfrom
azoxlpf:feat/add-schannel

Conversation

@azoxlpf

@azoxlpf azoxlpf commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR adds LDAP Schannel certificate authentication to NetExec via a new --schannel flag. When used with --pfx-cert (or --pem-cert / --pem-key), authentication is performed directly over LDAP/LDAPS using the client certificate, without going through PKINIT.

This PR depends on impacket changes from fortra/impacket#2205. That PR must be merged before this feature can work.

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)
  • This PR was created with the assistance of AI (list what type of assistance, tool(s)/model(s) in the description)

Setup guide for the review

run :

nxc ldap DC_IP -u user --pfx-cert user.pfx --schannel
nxc ldap DC_IP -u user --pfx-cert user.pfx --schannel --port 636

Screenshots (if appropriate):

image

Checklist:

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

  • I have ran Ruff against my changes (poetry: poetry run ruff check ., 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)
  • 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 linked relevant sources that describes the added technique (blog posts, documentation, etc)
  • I have performed a self-review of my own code (not an AI review)
  • 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 NeffIsBack added the enhancement New feature or request label Jun 9, 2026
@NeffIsBack

Copy link
Copy Markdown
Member

Very cool, thanks for the PR!

@NeffIsBack NeffIsBack left a comment

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.

Two thoughts:

  • Since we are authenticating with a cert the DC very likely has a TLS installed as well. So imo we should probably make schannel auth the default method and use PKINIT only when requested (as this is more likely to be unsupported imo). Thoughts?
  • Since we are using standard impacket ldap conn and auth please integrate it into the existing plaintext_login method.

Comment thread nxc/helpers/pfx.py Outdated
Comment thread nxc/cli.py Outdated
@azoxlpf

azoxlpf commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator Author

Two thoughts:

* Since we are authenticating with a cert the DC very likely has a TLS installed as well. So imo we should probably make schannel auth the default method and use PKINIT only when requested (as this is more likely to be unsupported imo). Thoughts?

* Since we are using standard impacket ldap conn and auth please integrate it into the existing `plaintext_login` method.

Done, Schannel auth is now integrated into plaintext_login.

On making Schannel the default: Schannel still requires either StartTLS or LDAPS, so in practice you need AD CS set up similarly to PKINIT. I have seen domains without StartTLS/LDAPS configured but with PKINIT working, though both setups are fairly rare. Since Schannel is LDAP-only and PKINIT works across all protocols, and with U2U we automatically unPAC the TGT to recover the NT hash, I’d keep PKINIT as the default for now personally

@NeffIsBack

Copy link
Copy Markdown
Member

On making Schannel the default: Schannel still requires either StartTLS or LDAPS, so in practice you need AD CS set up similarly to PKINIT. I have seen domains without StartTLS/LDAPS configured but with PKINIT working, though both setups are fairly rare. Since Schannel is LDAP-only and PKINIT works across all protocols, and with U2U we automatically unPAC the TGT to recover the NT hash, I’d keep PKINIT as the default for now personally

But when we are doing Schannel auth we do authenticate with a certificate. How realistic is it that we have a PKI (likely ADCS) with which we can authenticate, but the Domain Controller is missing the TLS cert? Isn't that quite unlikely when having a PKI connected to the domain in the first place? I would argue that this is much more unlikely than having PKINIT support.

@azoxlpf

azoxlpf commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator Author

On making Schannel the default: Schannel still requires either StartTLS or LDAPS, so in practice you need AD CS set up similarly to PKINIT. I have seen domains without StartTLS/LDAPS configured but with PKINIT working, though both setups are fairly rare. Since Schannel is LDAP-only and PKINIT works across all protocols, and with U2U we automatically unPAC the TGT to recover the NT hash, I’d keep PKINIT as the default for now personally

But when we are doing Schannel auth we do authenticate with a certificate. How realistic is it that we have a PKI (likely ADCS) with which we can authenticate, but the Domain Controller is missing the TLS cert? Isn't that quite unlikely when having a PKI connected to the domain in the first place? I would argue that this is much more unlikely than having PKINIT support.

Actually, that assumption can break in practice. I've hit an AD environment where LDAPS and StartTLS were both non-functional on the DC, yet PKINIT worked fine, I pulled off Shadow Credentials and UnPAC-the-Hash without any issue.

My hypothesis is that the PKI was hosted on Azure and not integrated as an on-prem Enterprise CA, so the DCs never auto-enrolled and never got a TLS server cert, which is why LDAPS was dead. But the CA cert had been manually published into the NTAuth store, which is enough for PKINIT to work on the client side. So you can absolutely have a working PKI with functional PKINIT and no Schannel at all.

azoxlpf added 2 commits June 19, 2026 12:14
Signed-off-by: Azox <213314124+azoxlpf@users.noreply.github.com>
Signed-off-by: Azox <213314124+azoxlpf@users.noreply.github.com>
@azoxlpf
azoxlpf force-pushed the feat/add-schannel branch 2 times, most recently from 5461bb0 to 9bd4e8e Compare June 19, 2026 10:31
@NeffIsBack

Copy link
Copy Markdown
Member

On making Schannel the default: Schannel still requires either StartTLS or LDAPS, so in practice you need AD CS set up similarly to PKINIT. I have seen domains without StartTLS/LDAPS configured but with PKINIT working, though both setups are fairly rare. Since Schannel is LDAP-only and PKINIT works across all protocols, and with U2U we automatically unPAC the TGT to recover the NT hash, I’d keep PKINIT as the default for now personally

But when we are doing Schannel auth we do authenticate with a certificate. How realistic is it that we have a PKI (likely ADCS) with which we can authenticate, but the Domain Controller is missing the TLS cert? Isn't that quite unlikely when having a PKI connected to the domain in the first place? I would argue that this is much more unlikely than having PKINIT support.

Actually, that assumption can break in practice. I've hit an AD environment where LDAPS and StartTLS were both non-functional on the DC, yet PKINIT worked fine, I pulled off Shadow Credentials and UnPAC-the-Hash without any issue.

My hypothesis is that the PKI was hosted on Azure and not integrated as an on-prem Enterprise CA, so the DCs never auto-enrolled and never got a TLS server cert, which is why LDAPS was dead. But the CA cert had been manually published into the NTAuth store, which is enough for PKINIT to work on the client side. So you can absolutely have a working PKI with functional PKINIT and no Schannel at all.

Okay sounds good, convinced :D One day we should probably also integrate PKINIT without UnPAC-the-hash so we can use the TGT directly. I don't think this is possible atm.

@azoxlpf

azoxlpf commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

On making Schannel the default: Schannel still requires either StartTLS or LDAPS, so in practice you need AD CS set up similarly to PKINIT. I have seen domains without StartTLS/LDAPS configured but with PKINIT working, though both setups are fairly rare. Since Schannel is LDAP-only and PKINIT works across all protocols, and with U2U we automatically unPAC the TGT to recover the NT hash, I’d keep PKINIT as the default for now personally

But when we are doing Schannel auth we do authenticate with a certificate. How realistic is it that we have a PKI (likely ADCS) with which we can authenticate, but the Domain Controller is missing the TLS cert? Isn't that quite unlikely when having a PKI connected to the domain in the first place? I would argue that this is much more unlikely than having PKINIT support.

Actually, that assumption can break in practice. I've hit an AD environment where LDAPS and StartTLS were both non-functional on the DC, yet PKINIT worked fine, I pulled off Shadow Credentials and UnPAC-the-Hash without any issue.
My hypothesis is that the PKI was hosted on Azure and not integrated as an on-prem Enterprise CA, so the DCs never auto-enrolled and never got a TLS server cert, which is why LDAPS was dead. But the CA cert had been manually published into the NTAuth store, which is enough for PKINIT to work on the client side. So you can absolutely have a working PKI with functional PKINIT and no Schannel at all.

Okay sounds good, convinced :D One day we should probably also integrate PKINIT without UnPAC-the-hash so we can use the TGT directly. I don't think this is possible atm.

Yeah, there's currently no real implementation of PKINIT, we're only using U2U but that's not the official implementation. I had started implementing RFC 4556 on Impacket, but it's taking a huge amount of time

Comment thread nxc/helpers/pfx.py Outdated
Comment thread nxc/protocols/ldap.py Outdated
Comment thread nxc/protocols/ldap.py
Comment thread nxc/cli.py Outdated
Comment thread nxc/protocols/ldap.py Outdated

@NeffIsBack NeffIsBack left a comment

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.

Some minor details left, after that we should be good. Logic looks good so far

Comment thread nxc/protocols/ldap.py Outdated
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=self.auth_choice != "simple", timeout=self.args.ldap_timeout)
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash, authenticationChoice=self.auth_choice)
authentication_choice = "external" if self.args.schannel else self.auth_choice
self.logger.info(f"Connecting to {ldap_url} using Schannel" if self.args.schannel else f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [3]")

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.

Feel free to add your using Schannel info, but please leave self.baseDN and self.host in the output. That can get pretty important for debugging (especially the latter).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment thread nxc/protocols/ldap.py Outdated
Comment on lines +472 to +475
if self.args.schannel:
mapped_user = self.get_ldap_username()
if mapped_user:
self.username = mapped_user

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 looks kinda redundant to line 443, is there a specific reason we need that? Shouldn't the authenticated user always be the specified one when we are using a pfx file?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ah right, I remember now why I did it this way: with schannel the -u isn't used for authentication, the cert is mapped to an account server-side, so a wrong -u still succeeds and would display the wrong user. The get_ldap_username() whoami is the only way to show the account we were actually mapped to.

Example with the wrong user in -u :

image

@NeffIsBack NeffIsBack Aug 2, 2026

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.

Hmm is there some way that we can extract/check the user in the cert? If possible we shouldn't allow such janky arg input. I think --pfc-cert also checks and restricts the specified user to the one in the certificate

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Hmm is there some way that we can extract/check the user in the cert? If possible we shouldn't allow such janky arg input. I think --pfc-cert also checks and restricts the specified user to the one in the certificate

Yeah we can extract it (SAN UPN, else dNSName, else subject CN). One thing to consider though: with Schannel the mapping is done by the DC, and explicit altSecurityIdentities mappings carry no UPN at all, so we'd reject valid certs.

Also --pfx-cert doesn't check on our side either, it just puts -u in the AS-REQ cname and lets the KDC reject it.

Another option would be to stop requiring -u with --schannel and rely on the whoami result, which removes the janky input entirely. What do you think?

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.

Hmm okay makes sense. Yeah then let's get the user by doing the whoami. Then it is similar to --use-kcache where the user is ignored as well

Comment thread nxc/connection.py Outdated
Comment thread nxc/protocols/ldap.py
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.

3 participants