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

Add TLS channel binding during NTLM authentication #1087

Merged

Conversation

ThePirateWhoSmellsOfSunflowers
Copy link
Contributor

Based on #1042, this PR adds TLS channel binding (tls-server-end-point) support. The idea is to bind the outer secure connection (TLS in our case) to application data over an inner client-authenticated channel (NTLM here). This kind of channel binding seems to be the only suported by Microsoft Active Directory during NTLM authentication.

To perform channel binding during NTLM authentication, we need to add a new AV_PAIR MS-NLMP 2.2.2.1 within the AUTHENTICATE_MESSAGE MS-NLMP 2.2.1.3. This new AV_PAIR has AvId 0x000A (MsvAvChannelBindings). The Value field contains an MD5 hash of a gss_channel_bindings_struct. Basicaly we just have to put the sha256 of the server's certificate within this struct.

This PR also fixes a bug in the method pack_av_info() within NtlmClient class.

The logic for this PR is heavly inspired by "msldap", "minikerberos" and "asysocks" projects by @skelsec.

🌻

ThePirateWhoSmellsOfSunflowers added a commit to the-useless-one/pywerview that referenced this pull request May 19, 2023
@hughpyle
Copy link

hughpyle commented Jun 8, 2023

This works well for my use case (access to Active Directory with LdapEnforceChannelBinding set to "Always").

@ThePirateWhoSmellsOfSunflowers
Copy link
Contributor Author

Hi @Neustradamus!

Thanks for the feedback! Unfortunately, my skills are limited to python and i'm just interested in "offensive" LDAP, so i don't plan to port it on other projects.

🌻

@Neustradamus
Copy link

@ThePirateWhoSmellsOfSunflowers: Thanks for your answer :)
I understand no problem, I hope that your improvement will be added quickly!

dadevel added a commit to dadevel/impacket that referenced this pull request Nov 28, 2023
Use github.com/cannatag/ldap3#1087 to add LDAP Channel Binding support to the RBCD example script.
dadevel added a commit to dadevel/impacket that referenced this pull request Nov 28, 2023
Use cannatag/ldap3#1087 to add LDAP Channel Binding support to the RBCD example script.
dadevel added a commit to dadevel/impacket that referenced this pull request Nov 28, 2023
Use cannatag/ldap3#1087 to add LDAP Channel Binding support to the RBCD example script.
dadevel added a commit to dadevel/impacket that referenced this pull request Nov 28, 2023
Use cannatag/ldap3#1087 to add LDAP Channel Binding support to the RBCD example script.
@tomspencer
Copy link

Any update on this? Requiring channel binding is quickly becoming the standard so this support would be hugely valuable.

@Augustin-FL
Copy link
Contributor

hi @cannatag
possible to merge this PR?

@cannatag
Copy link
Owner

cannatag commented Mar 6, 2024 via email

@ThePirateWhoSmellsOfSunflowers
Copy link
Contributor Author

Hi @cannatag!
Thanks for your message, is it possible to also take a look at #1042? Thanks

🌻

@cannatag
Copy link
Owner

cannatag commented Mar 6, 2024 via email

@Neustradamus
Copy link

@ThePirateWhoSmellsOfSunflowers, @cannatag: Good news, soon one year of this PR!

@cannatag cannatag merged commit d3de6af into cannatag:dev Mar 19, 2024
@cannatag
Copy link
Owner

Thanks!

@cannatag
Copy link
Owner

Can you check the 2.10 version in dev? I don't have access to AD so I cannot check if this PR is working as expected.

Thanks,
Giovanni

@ThePirateWhoSmellsOfSunflowers
Copy link
Contributor Author

Hello @cannatag,

Whoa thanks for the mass merging!
The PR is broken but I submitted a patch (see #1130), should work now.

🌻

@dadevel
Copy link

dadevel commented Apr 24, 2024

Is it possible to tag a new release that includes this PR? Would be nice for downstream projects (CC fortra/impacket#1657).

@BotoX
Copy link

BotoX commented Jul 9, 2024

For some reason I still can not connect to the active directory servers at our company after installing the latest ldap3:
pip install --upgrade git+https://github.com/ThePirateWhoSmellsOfSunflowers/ldap3@dev

My code looks like this:

        domain['_server'] = Server('AD_DOMAIN.COMPANY.COM', port=636, use_ssl=True, get_info=ALL)
        domain['_conn'] = Connection(domain['_server'], user=AD_USERNAME, password=AD_PASSWORD, channel_binding=TLS_CHANNEL_BINDING, authentication=NTLM, session_security=ENCRYPT, raise_exceptions=True)
        domain['_conn'].bind()

output:

DEBUG:ldap3:ERROR:detail level set to BASIC
DEBUG:ldap3:BASIC:instantiated Tls: <Tls(validate=<VerifyMode.CERT_NONE: 0>)>
DEBUG:ldap3:BASIC:instantiated Server: <Server(host='AD_DOMAIN.COMPANY.COM', port=636, use_ssl=True, allowed_referral_hosts=[('*', True)], tls=Tls(validate=<VerifyMode.CERT_NONE: 0>), get_info='ALL', mode='IP_V6_PREFERRED')>
DEBUG:ldap3:BASIC:instantiated <SyncStrategy>: <ldaps://AD_DOMAIN.COMPANY.COM:636 - ssl - user: AD_USERNAME - not lazy - unbound - closed - <no socket> - tls not started - not listening - No strategy - internal decoder - async - real DSA - not pooled - cannot stream output>
DEBUG:ldap3:BASIC:instantiated Connection: <Connection(server=Server(host='AD_DOMAIN.COMPANY.COM', port=636, use_ssl=True, allowed_referral_hosts=[('*', True)], tls=Tls(validate=<VerifyMode.CERT_NONE: 0>), get_info='ALL', mode='IP_V6_PREFERRED'), user='EDVZ\\sdhpcsetup', password='<stripped 20 characters of sensitive data>',session_security='ENCRYPT', auto_bind='DEFAULT', version=3, authentication='NTLM', client_strategy='SYNC', auto_referrals=True, check_names=True, read_only=False, lazy=False, raise_exceptions=True, fast_decoder=True, auto_range=True, return_empty_attributes=True, auto_encode=True, auto_escape=True, use_referral_cache=False)>
DEBUG:ldap3:BASIC:start BIND operation via <ldaps://AD_DOMAIN.COMPANY.COM:636 - ssl - user: AD_USERNAME - not lazy - unbound - closed - <no socket> - tls not started - not listening - SyncStrategy - internal decoder>
DEBUG:ldap3:BASIC:address for <ldaps://AD_DOMAIN.COMPANY.COM:636 - ssl> resolved as <[<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('123.45.14.74', 636)]>
DEBUG:ldap3:BASIC:address for <ldaps://AD_DOMAIN.COMPANY.COM:636 - ssl> resolved as <[<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('123.45.14.75', 636)]>
DEBUG:ldap3:BASIC:address for <ldaps://AD_DOMAIN.COMPANY.COM:636 - ssl> resolved as <[<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('123.45.14.76', 636)]>
DEBUG:ldap3:BASIC:obtained candidate address for <ldaps://AD_DOMAIN.COMPANY.COM:636 - ssl>: <[<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('123.45.14.74', 636)]> with mode IP_V6_PREFERRED
DEBUG:ldap3:BASIC:obtained candidate address for <ldaps://AD_DOMAIN.COMPANY.COM:636 - ssl>: <[<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('123.45.14.75', 636)]> with mode IP_V6_PREFERRED
DEBUG:ldap3:BASIC:obtained candidate address for <ldaps://AD_DOMAIN.COMPANY.COM:636 - ssl>: <[<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('123.45.14.76', 636)]> with mode IP_V6_PREFERRED
DEBUG:ldap3:BASIC:try to open candidate address [<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('123.45.14.74', 636)]
DEBUG:ldap3:BASIC:start NTLM BIND operation via <ldaps://AD_DOMAIN.COMPANY.COM:636 - ssl - user: AD_USERNAME - not lazy - unbound - open - <local: 123.45.10.42:52353 - remote: 123.45.14.74:636> - tls not started - listening - SyncStrategy - internal decoder>
Traceback (most recent call last):
  File "./ad_hpc_user.py", line 163, in <module>
    pers_conn = connect_domain(domains['pers'])
  File "./ad_hpc_user.py", line 80, in connect_domain
    domain['_conn'].bind()
  File "/usr/local/lib/python3.8/dist-packages/ldap3/core/connection.py", line 655, in bind
    response = self.do_ntlm_bind(controls)
  File "/usr/local/lib/python3.8/dist-packages/ldap3/core/connection.py", line 1459, in do_ntlm_bind
    response = self.post_send_single_response(self.send('bindRequest', request, controls))
  File "/usr/local/lib/python3.8/dist-packages/ldap3/strategy/sync.py", line 179, in post_send_single_response
    responses, result = self.get_response(message_id)
  File "/usr/local/lib/python3.8/dist-packages/ldap3/strategy/base.py", line 417, in get_response
    raise LDAPOperationResult(result=result['result'], description=result['description'], dn=result['dn'], message=result['message'], response_type=result['type'])
ldap3.core.exceptions.LDAPInvalidCredentialsResult: LDAPInvalidCredentialsResult - 49 - invalidCredentials - None - 80090346: LdapErr: DSID-0C0907FB, comment: AcceptSecurityContext error, data 80090346, v4f7c - bindResponse - None

I'm definitely using the new version of the library, as I can trigger the following exception:

  File "/usr/local/lib/python3.8/dist-packages/ldap3/core/connection.py", line 345, in __init__
    raise LDAPInvalidValueError(self.last_error)
ldap3.core.exceptions.LDAPInvalidValueError: "channel_binding" option only available for NTLM authentication over LDAPS

@ThePirateWhoSmellsOfSunflowers
Copy link
Contributor Author

ThePirateWhoSmellsOfSunflowers commented Jul 9, 2024

Hi @BotoX!

Not sure why but in your snippet you use both channel_binding=TLS_CHANNEL_BINDING and session_security=ENCRYPT, those two options are not compatible because on is for Channel Binding and the other one is for LDAP Signing.

🌻

@BotoX
Copy link

BotoX commented Jul 9, 2024

Hi @BotoX!

Not sure why but in your snippet you use both channel_binding=TLS_CHANNEL_BINDING and session_security=ENCRYPT, those two options are not compatible because on is for Channel Binding and the other one is for LDAP Signing.

🌻

Hey @ThePirateWhoSmellsOfSunflowers, thanks for the quick response.
I've added session_security=ENCRYPT just to see if it changes anything. (More security / encryption = better, right?)
Sadly it makes no difference when I remove it, the exact same error persists.

To be clear my connection code looks like this now:

        domain['_server'] = Server('AD_DOMAIN.COMPANY.COM', port=636, use_ssl=True, get_info=ALL)
        domain['_conn'] = Connection(domain['_server'], user=AD_USERNAME, password=AD_PASSWORD, channel_binding=TLS_CHANNEL_BINDING, authentication=NTLM, raise_exceptions=True)
        domain['_conn'].bind()

and I get the exact same log output as above (except for session_security='ENCRYPT' in the Connection object).

@ThePirateWhoSmellsOfSunflowers
Copy link
Contributor Author

Mhhh hard to blind debug, sorry.

Last thing, can you try something like

$ openssl s_client -connect AD_DOMAIN.COMPANY.COM:636 -servername AD_DOMAIN.COMPANY.COM 2>/dev/null | openssl x509 -noout -text | grep 'Signature Algorithm'

(source)

According to the RFC

Description: The hash of the TLS server's certificate [RFC5280] as it
appears, octet for octet, in the server's Certificate message. Note
that the Certificate message contains a certificate_list, in which
the first element is the server's certificate.

The hash function is to be selected as follows:

o if the certificate's signatureAlgorithm uses a single hash
function, and that hash function is either MD5 [RFC1321] or SHA-1
[RFC3174], then use SHA-256 [FIPS-180-3];

o if the certificate's signatureAlgorithm uses a single hash
function and that hash function neither MD5 nor SHA-1, then use
the hash function associated with the certificate's
signatureAlgorithm;

o if the certificate's signatureAlgorithm uses no hash functions or
uses multiple hash functions, then this channel binding type's
channel bindings are undefined at this time (updates to is channel
binding type may occur to address this issue if it ever arises).

Maybe your certificate is not signed with MD5, SHA1 or SHA256 ?

🌻

@BotoX
Copy link

BotoX commented Jul 11, 2024

Maybe your certificate is not signed with MD5, SHA1 or SHA256 ?

You are correct:

        Signature Algorithm: sha384WithRSAEncryption
    Signature Algorithm: sha384WithRSAEncryption

@ThePirateWhoSmellsOfSunflowers
Copy link
Contributor Author

Nice!
So now you can to change this part

ldap3/ldap3/core/connection.py

Lines 1407 to 1417 in 86a9e7a

from hashlib import sha256, md5
self.ntlm_client.tls_channel_binding = True
peer_certificate_sha256 = sha256(self.server.tls.peer_certificate).digest()
# https://datatracker.ietf.org/doc/html/rfc2744#section-3.11
channel_binding_struct = bytes()
initiator_address = b'\x00'*8
acceptor_address = b'\x00'*8
# https://datatracker.ietf.org/doc/html/rfc5929#section-4
application_data_raw = b'tls-server-end-point:' + peer_certificate_sha256

with

 from hashlib import sha384, md5 
 self.ntlm_client.tls_channel_binding = True 
 peer_certificate_sha384 = sha384(self.server.tls.peer_certificate).digest() 
  
 # https://datatracker.ietf.org/doc/html/rfc2744#section-3.11 
 channel_binding_struct = bytes() 
 initiator_address = b'\x00'*8 
 acceptor_address = b'\x00'*8 
  
 # https://datatracker.ietf.org/doc/html/rfc5929#section-4 
 application_data_raw = b'tls-server-end-point:' + peer_certificate_sha384

I'll submit a better PR if it works.

🌻

@ThePirateWhoSmellsOfSunflowers
Copy link
Contributor Author

can you test this branch please ?

https://github.com/ThePirateWhoSmellsOfSunflowers/ldap3/tree/fix_server_to_endpoint_hash

(don't forget to add the cryptography package)

🌻

@BotoX
Copy link

BotoX commented Jul 12, 2024

can you test this branch please ?

https://github.com/ThePirateWhoSmellsOfSunflowers/ldap3/tree/fix_server_to_endpoint_hash

Thank you @ThePirateWhoSmellsOfSunflowers

I can connect to the active directory / ldap with that branch now 👍

Had to apply this patch in order to install it on Linux though: 8eca7c8

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

Successfully merging this pull request may close these issues.

None yet

8 participants