Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LDAPS Channel Binding for NTLM and Kerberos #19132

Merged
merged 8 commits into from May 13, 2024

Conversation

zeroSteiner
Copy link
Contributor

@zeroSteiner zeroSteiner commented Apr 24, 2024

Background

In 2020, Microsoft started adding options to harden the conifugraiton of LDAP on AD DS servers (domain controllers). These settings included channel binding and signing requirements. Microsoft posted an article here with additional details.

This pull request adds channel binding information to Metasploit's NTLM and Kerberos authentication for the LDAP protocol. This enables users to authenticate to domain controllers where the hardened security configuration setting is in place (LdapEnforceChannelBinding = 2). Prior to these changes, when a user attempted to authenticate to an LDAPS service where channel binding was required, the authentication attempt would fail with an error that the credentials were invalid. This error is of course misleading because the credentials are correct, but authentication failed for other reasons. With these changes in place, the authenticaiton will succeed without any additional changes from the user.

Implementation

The channel binding is the MD5 hash of the gss_channel_bindings_struct which itself includes a hash of the peer's TLS certificate. The hashing algorithm of the peer's certificiate is "almost always SHA256" but can be different in certain scenarios as described here. The channel binding token is the same for NTLM and for Kerberos, however it's placed in different locations of the negotiation frames. For NTLM it's an additional AV_PAIR structure, while for Kerberos, it's included in the checksum field of the AP-REQ structure.

The actual authentication logic was moved into dedicated adapter classes, which register themselves with the Net::LDAP library as rex_ntlm and rex_kerberos. This cleans up the code a bit, makes it reusable but most importantly is necessary to expose the connections #peer_cert for the channel bindings.

This PR also adds some updates to the error handling for a few of the LDAP modules to avoid stack traces for various connection related errors.

Testing Steps

  • Set the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters\LdapEnforceChannelBinding registry setting to 2 for "Always"
    • At this point, authentication should fail when targing the domain controller over LDAPS (RPORT=636 and SSL=true)
  • Use a module that performs LDAP authentication such as auxiliary/gather/ldap_query
  • Set the generic options as appropriate (RHOSTS, USERNAME, PASSWORD, DOMAIN)
  • Test the module still works with LDAP and NTLM authentication (run LDAP::Auth=ntlm RPORT=389 SSL=false)
  • Test the module works with LDAPS and NTLM authentication with channel binding (run LDAP::Auth=ntlm RPORT=636 SSL=true)
  • Test the module still works with LDAP and Kerberos authentication (run LDAP::Auth=kerberos RPORT=389 SSL=false)
  • Test the module works with LDAPS and Kerberos authentication with channel binding (run LDAP::Auth=kerberos RPORT=636 SSL=true)

References

The following are various resources that were valuable references while implementing these changes.

@Neustradamus
Copy link

To follow!

@adfoster-r7
Copy link
Contributor

It's a shame we can't add this to our automated ldap/samba tests - https://wiki.samba.org/index.php/Configuring_LDAP_over_SSL_(LDAPS)_on_a_Samba_AD_DC

Samba doesn't implement LDAP Channel binding as required by the 2020 LDAP channel binding and LDAP signing requirements for Windows. Instead, in 2016 with CVE-2016-2112 we recognised the with no cryptographic connection between the NTLM response or Kerberos token and the TLS layer, that a relay attack was possible.

Samba has chosen to simply deny such sessions by default.

@smcintyre-r7 smcintyre-r7 added the blocked Blocked by one or more additional tasks label May 3, 2024
@zeroSteiner
Copy link
Contributor Author

Marked this as blocked because I think we'll want to land #19127 which will create conflicts. At that point, I'll rebase this work on top of those changes.

@smcintyre-r7 smcintyre-r7 mentioned this pull request May 6, 2024
15 tasks
@zeroSteiner zeroSteiner force-pushed the feat/lib/ldap-channel-binding branch from a889a8b to 2bf402f Compare May 8, 2024 21:05
@smcintyre-r7 smcintyre-r7 removed the blocked Blocked by one or more additional tasks label May 8, 2024
@zeroSteiner
Copy link
Contributor Author

zeroSteiner commented May 8, 2024

I ran through all the combinations I could think of using this resource script against a DC which required signing and channel binding. Everything I expected to work did work. Cases that failed were expected to fail for the following reasons:

  • Anytime the authentication is set to plaintext or schannel, authentication will fail because neither signing nor channel binding work with those authentication mechanisms
  • LDAP::Signing=required and SSL=true fails because you can't sign an LDAPS connection to AD DS, the domain controller will reject it
  • LDAP::Signing=disabled fails because the server is configured to require signatures, at this time channel binding can't be disabled through a datastore option in the same way but maybe a LDAP::ChannelBinding option would be a good idea
ldap_test.rc
run LDAP::Auth=plaintext LDAP::Signing=disabled RPORT=389 SSL=false
run LDAP::Auth=plaintext LDAP::Signing=disabled RPORT=636 SSL=true
run LDAP::Auth=plaintext LDAP::Signing=auto RPORT=389 SSL=false
run LDAP::Auth=plaintext LDAP::Signing=auto RPORT=636 SSL=true
run LDAP::Auth=plaintext LDAP::Signing=required RPORT=389 SSL=false
run LDAP::Auth=plaintext LDAP::Signing=required RPORT=636 SSL=true
run LDAP::Auth=kerberos LDAP::Signing=disabled RPORT=389 SSL=false
run LDAP::Auth=kerberos LDAP::Signing=disabled RPORT=636 SSL=true
run LDAP::Auth=kerberos LDAP::Signing=auto RPORT=389 SSL=false
run LDAP::Auth=kerberos LDAP::Signing=auto RPORT=636 SSL=true
run LDAP::Auth=kerberos LDAP::Signing=required RPORT=389 SSL=false
run LDAP::Auth=kerberos LDAP::Signing=required RPORT=636 SSL=true
run LDAP::Auth=ntlm LDAP::Signing=disabled RPORT=389 SSL=false
run LDAP::Auth=ntlm LDAP::Signing=disabled RPORT=636 SSL=true
run LDAP::Auth=ntlm LDAP::Signing=auto RPORT=389 SSL=false
run LDAP::Auth=ntlm LDAP::Signing=auto RPORT=636 SSL=true
run LDAP::Auth=ntlm LDAP::Signing=required RPORT=389 SSL=false
run LDAP::Auth=ntlm LDAP::Signing=required RPORT=636 SSL=true

@jheysel-r7 jheysel-r7 self-assigned this May 10, 2024
Copy link
Contributor

@jheysel-r7 jheysel-r7 left a comment

Choose a reason for hiding this comment

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

Great work 👍 After a thorough review I think this is good to land. Testing was as expected:

Testing on current upstream-master

With LdapEnforceChannelBinding = 2, the module fails to connect when valid credentials are provided: Invalid credentials provided!

msf6 auxiliary(gather/ldap_query) > run rport=636 ssl=true rhosts=3.22.14.197 username=jheysel password=ChangeM3! domain=LABS1COLLABU0 action=ENUM_DOMAIN
[*] Running module against 3.22.14.197

[-] Auxiliary aborted due to failure: no-access: Invalid credentials provided!
[*] Auxiliary module execution completed

Testing on this PR (upstream/pr/19132)

With the changes in this PR now the module is able to successfully connect with the same (correct) credentials when LdapEnforceChannelBinding = 2

msf6 auxiliary(gather/ldap_query) > run rport=636 ssl=true rhosts=3.22.14.197 username=jheysel password=ChangeM3! domain=LABS1COLLABU0 action=ENUM_DOMAIN
[*] Running module against 3.22.14.197

[+] Successfully bound to the LDAP server!
[*] Discovering base DN automatically
[*] 3.22.14.197:636 Getting root DSE
[+] 3.22.14.197:636 Discovered base DN: DC=labs1collabu0,DC=local
DC=labs1collabu0,DC=local
=========================

 Name                       Attributes
 ----                       ----------
 lockoutduration            0:00:10:00
 lockoutthreshold           0
 maxpwdage                  10675199:02:48:05
 minpwdage                  0:00:00:00
 minpwdlength               0
 ms-ds-machineaccountquota  10
 name                       labs1collabu0
 objectsid                  S-1-5-21-795503-3050334394-3644400624

[*] Query returned 1 result.
[*] Auxiliary module execution completed

The module still works with LDAP and NTLM authentication:

msf6 auxiliary(gather/ldap_query) > run LDAP::Auth=ntlm RPORT=389 SSL=false rhosts=3.22.14.197 username=jheysel password=ChangeM3! domain=LABS1COLLABU0 action=ENUM_DOMAIN
[*] Running module against 3.22.14.197

[*] Discovering base DN automatically
[+] 3.22.14.197:389 Discovered base DN: DC=labs1collabu0,DC=local
DC=labs1collabu0,DC=local
=========================

 Name                       Attributes
 ----                       ----------
 lockoutduration            0:00:10:00
 lockoutthreshold           0
 maxpwdage                  10675199:02:48:05
 minpwdage                  0:00:00:00
 minpwdlength               0
 ms-ds-machineaccountquota  10
 name                       labs1collabu0
 objectsid                  S-1-5-21-795503-3050334394-3644400624

[*] Query returned 1 result.
[*] Auxiliary module execution completed

The module still works with LDAPS and NTLM authentication with channel binding

msf6 auxiliary(gather/ldap_query) > run LDAP::Auth=ntlm RPORT=636 SSL=true rhosts=3.22.14.197 username=jheysel password=ChangeM3! domain=LABS1COLLABU0 action=ENUM_DOMAIN
[*] Running module against 3.22.14.197

[*] Discovering base DN automatically
[+] 3.22.14.197:636 Discovered base DN: DC=labs1collabu0,DC=local
DC=labs1collabu0,DC=local
=========================

 Name                       Attributes
 ----                       ----------
 lockoutduration            0:00:10:00
 lockoutthreshold           0
 maxpwdage                  10675199:02:48:05
 minpwdage                  0:00:00:00
 minpwdlength               0
 ms-ds-machineaccountquota  10
 name                       labs1collabu0
 objectsid                  S-1-5-21-795503-3050334394-3644400624

[*] Query returned 1 result.
[*] Auxiliary module execution completed

The module still works with LDAP and Kerberos authentication:

msf6 auxiliary(gather/ldap_query) > run LDAP::Auth=kerberos LDAP::Rhostname=srv-adds01.labs1collabu0.local DomainControllerRhost=3.22.14.197 RPORT=389 SSL=false rhosts=3.22.14.197 username=jheysel password=ChangeM3! domain=labs1collabu0.local action=ENUM_DOMAIN
[*] Running module against 3.22.14.197

[+] 3.22.14.197:88 - Received a valid TGT-Response
[*] 3.22.14.197:389 - TGT MIT Credential Cache ticket saved to /Users/jheysel/.msf4/loot/20240510131946_default_3.22.14.197_mit.kerberos.cca_737339.bin
[+] 3.22.14.197:88 - Received a valid TGS-Response
[*] 3.22.14.197:389 - TGS MIT Credential Cache ticket saved to /Users/jheysel/.msf4/loot/20240510131946_default_3.22.14.197_mit.kerberos.cca_520137.bin
[+] 3.22.14.197:88 - Received a valid delegation TGS-Response
[+] 3.22.14.197:88 - Received AP-REQ. Extracting session key...
[*] Discovering base DN automatically
[+] 3.22.14.197:389 Discovered base DN: DC=labs1collabu0,DC=local
DC=labs1collabu0,DC=local
=========================

 Name                       Attributes
 ----                       ----------
 lockoutduration            0:00:10:00
 lockoutthreshold           0
 maxpwdage                  10675199:02:48:05
 minpwdage                  0:00:00:00
 minpwdlength               0
 ms-ds-machineaccountquota  10
 name                       labs1collabu0
 objectsid                  S-1-5-21-795503-3050334394-3644400624

[*] Query returned 1 result.
[*] Auxiliary module execution **completed**

 The module still works with LDAPS and Kerberos authentication with channel binding

msf6 auxiliary(gather/ldap_query) > run LDAP::Auth=kerberos LDAP::Rhostname=srv-adds01.labs1collabu0.local DomainControllerRhost=3.22.14.197 RPORT=636 SSL=true rhosts=3.22.14.197 username=jheysel password=ChangeM3! domain=labs1collabu0.local action=ENUM_DOMAIN
[*] Running module against 3.22.14.197

[+] 3.22.14.197:88 - Received a valid TGT-Response
[*] 3.22.14.197:636 - TGT MIT Credential Cache ticket saved to /Users/jheysel/.msf4/loot/20240510132411_default_3.22.14.197_mit.kerberos.cca_670902.bin
[+] 3.22.14.197:88 - Received a valid TGS-Response
[*] 3.22.14.197:636 - TGS MIT Credential Cache ticket saved to /Users/jheysel/.msf4/loot/20240510132411_default_3.22.14.197_mit.kerberos.cca_809948.bin
[+] 3.22.14.197:88 - Received a valid delegation TGS-Response
[*] Discovering base DN automatically
[+] 3.22.14.197:636 Discovered base DN: DC=labs1collabu0,DC=local
DC=labs1collabu0,DC=local
=========================

 Name                       Attributes
 ----                       ----------
 lockoutduration            0:00:10:00
 lockoutthreshold           0
 maxpwdage                  10675199:02:48:05
 minpwdage                  0:00:00:00
 minpwdlength               0
 ms-ds-machineaccountquota  10
 name                       labs1collabu0
 objectsid                  S-1-5-21-795503-3050334394-3644400624

[*] Query returned 1 result.
[*] Auxiliary module execution completed

@jheysel-r7 jheysel-r7 merged commit b1cd5b3 into rapid7:master May 13, 2024
50 checks passed
@jheysel-r7
Copy link
Contributor

Release Notes

Add channel binding information to Metasploit's NTLM and Kerberos authentication for the LDAP protocol. This enables users to authenticate to domain controllers where the hardened security configuration setting is in place

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants