Implement starttls#36
Conversation
|
pywhisker is using ldap3 which does not seem to support username+password or NTLM hash when LDAP signing is enforced: |
There was a problem hiding this comment.
Pull request overview
Adds LDAP STARTTLS support to pyWhisker so it can upgrade a plaintext LDAP connection (389) to TLS when LDAPS (636) isn’t available but the DC enforces signing/TLS.
Changes:
- Introduces
--use-starttlsand a new STARTTLS-based LDAP connection initializer. - Improves bind failure error handling for NTLM binds and Kerberos SASL bind failures with actionable guidance.
- Updates README usage options to mention the new STARTTLS flag.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
pywhisker/pywhisker.py |
Adds STARTTLS connection path, wiring via --use-starttls, and improves bind error messaging. |
README.md |
Documents the new --use-starttls CLI option. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ldap_session.open() | ||
| ldap_session.start_tls() | ||
| ldap_session.bind() | ||
| ldap3_kerberos_login(ldap_session, target, username, password, logger, domain, lmhash, nthash, args.auth_key, kdcHost=args.dc_ip) |
There was a problem hiding this comment.
start_tls() is called but its return value isn’t checked. In ldap3, STARTTLS negotiation can fail and return False without raising, which would cause the subsequent bind() to send credentials over an unencrypted connection. Check the result of start_tls() (and/or ldap_session.tls_started) and raise an error before binding if TLS was not successfully established.
| return init_ldap_starttls_connection(target, ssl.PROTOCOL_TLSv1_2, args, domain, username, password, lmhash, nthash, logger) | ||
| except ldap3.core.exceptions.LDAPSocketOpenError: | ||
| return init_ldap_starttls_connection(target, ssl.PROTOCOL_TLSv1, args, domain, username, password, lmhash, nthash, logger) |
There was a problem hiding this comment.
The STARTTLS TLS-version fallback only catches LDAPSocketOpenError, but STARTTLS negotiation/handshake failures typically surface as start_tls() returning False or raising a STARTTLS/TLS-related exception instead. This can prevent fallback and/or lead to confusing errors. Consider checking the start_tls() result and broadening the exception handling to include STARTTLS/TLS handshake failures (and surface a clear error) before retrying with the older TLS version.
| return init_ldap_starttls_connection(target, ssl.PROTOCOL_TLSv1_2, args, domain, username, password, lmhash, nthash, logger) | |
| except ldap3.core.exceptions.LDAPSocketOpenError: | |
| return init_ldap_starttls_connection(target, ssl.PROTOCOL_TLSv1, args, domain, username, password, lmhash, nthash, logger) | |
| return init_ldap_starttls_connection( | |
| target, | |
| ssl.PROTOCOL_TLSv1_2, | |
| args, | |
| domain, | |
| username, | |
| password, | |
| lmhash, | |
| nthash, | |
| logger, | |
| ) | |
| except ( | |
| ldap3.core.exceptions.LDAPSocketOpenError, | |
| ldap3.core.exceptions.LDAPStartTLSError, | |
| ssl.SSLError, | |
| ) as e: | |
| if logger is not None: | |
| logger.warning( | |
| f"[!] STARTTLS failed with TLSv1_2 for {target}: {e}. Retrying with TLSv1." | |
| ) | |
| try: | |
| return init_ldap_starttls_connection( | |
| target, | |
| ssl.PROTOCOL_TLSv1, | |
| args, | |
| domain, | |
| username, | |
| password, | |
| lmhash, | |
| nthash, | |
| logger, | |
| ) | |
| except ( | |
| ldap3.core.exceptions.LDAPSocketOpenError, | |
| ldap3.core.exceptions.LDAPStartTLSError, | |
| ssl.SSLError, | |
| ) as e2: | |
| raise Exception( | |
| f"[ERROR] Failed to establish STARTTLS LDAP connection to {target} " | |
| f"using both TLSv1_2 and TLSv1: {e2}" | |
| ) |
| parser.add_argument("-a", "--action", choices=['list', 'add', 'spray', 'remove', 'clear', 'info', 'export', 'import'], nargs='?', default='list', help='Action to operate on msDS-KeyCredentialLink') | ||
| parser.add_argument('--use-ldaps', action='store_true', help='Use LDAPS instead of LDAP') | ||
| parser.add_argument('--use-starttls', action='store_true', help='Use STARTTLS for LDAP connection upgrade') | ||
| parser.add_argument('--use-schannel', action='store_true', help='Use LDAP Schannel (TLS) for certificate-based authentication') |
There was a problem hiding this comment.
--use-starttls, --use-ldaps, and --use-schannel can currently be provided together; the code will silently prioritize one path (e.g., STARTTLS wins over LDAPS). This is ambiguous for users and can mask misconfiguration. Consider making these options mutually exclusive in argparse (or validating after parsing) and erroring out when more than one transport mode is selected.
| -a [{list,add,spray,remove,clear,info,export,import}], --action [{list,add,spray,remove,clear,info,export,import}] | ||
| Action to operate on msDS-KeyCredentialLink | ||
| --use-ldaps Use LDAPS instead of LDAP | ||
| --use-starttls Use STARTTLS for LDAP connection upgrade |
There was a problem hiding this comment.
The README’s detailed options list was updated with --use-starttls, but the usage synopsis earlier in the same code block still omits this flag. Please update the synopsis line as well so the README output matches the current CLI.
Implementation of LDAP STARTTLS. Useful if LDAPS in unavailable while DC still requires signing/TLS
Get kerberos ticket: