-
Notifications
You must be signed in to change notification settings - Fork 756
add Schannel certificate authentication via --schannel #1277
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
base: main
Are you sure you want to change the base?
Changes from 8 commits
e8ad952
59c032e
812c63e
7fcb9b2
c94d00f
9bd4e8e
f6e85c8
6c59c7a
98ae72f
d97907b
5bd45d2
7ca004b
163139b
d950a5c
14f10c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,7 @@ | |
| from nxc.config import process_secret, host_info_colors | ||
| from nxc.connection import connection | ||
| from nxc.helpers.bloodhound import add_user_bh | ||
| from nxc.helpers.pfx import pfx_to_pem_files | ||
| from nxc.helpers.misc import get_bloodhound_info, convert, d2b, parse_argument | ||
| from nxc.logger import NXCAdapter | ||
| from nxc.protocols.ldap.bloodhound import BloodHound, resolve_collection_methods | ||
|
|
@@ -451,21 +452,44 @@ def plaintext_login(self, domain, username, password): | |
| hash_asreproast.write(f"{hash_tgt}\n") | ||
| return False | ||
|
|
||
| cert_file = key_file = None | ||
| if self.args.schannel: | ||
| cert_file, key_file = pfx_to_pem_files(self) | ||
| if not cert_file: | ||
| return False | ||
|
|
||
| try: | ||
| # Connect to LDAP | ||
| self.logger.extra["protocol"] = "LDAPS" if self.port == 636 else "LDAP" | ||
| self.logger.extra["port"] = "636" if self.port == 636 else "389" | ||
| proto = "ldaps" if self.port == 636 else "ldap" | ||
| ldap_url = f"{proto}://{self.target}" | ||
| self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [3]") | ||
| 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 | ||
| conn_kwargs = {"url": ldap_url, "baseDN": self.baseDN, "dstIp": self.host, "signing": self.auth_choice != "simple", "timeout": self.args.ldap_timeout} | ||
| if self.args.schannel: | ||
| conn_kwargs["certfile"] = cert_file | ||
| conn_kwargs["keyfile"] = key_file | ||
|
|
||
| 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]") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feel free to add your
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| self.ldap_connection = ldap_impacket.LDAPConnection(**conn_kwargs) | ||
|
NeffIsBack marked this conversation as resolved.
Outdated
|
||
| self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash, authenticationChoice=authentication_choice) | ||
|
|
||
| if self.args.schannel: | ||
| mapped_user = self.get_ldap_username() | ||
| if mapped_user: | ||
| self.username = mapped_user | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Example with the wrong user in -u :
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah we can extract it ( Also Another option would be to stop requiring
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| self.check_if_admin() | ||
| self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}") | ||
| self.db.add_credential("plaintext", domain, self.username, self.password) | ||
|
|
||
| # Prepare success credential text | ||
| self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}") | ||
| if self.args.schannel: | ||
| self.logger.debug(f"Adding credential: {self.domain}/{self.username} from certificate") | ||
| self.db.add_credential("certificate", self.domain, self.username, "") | ||
| self.logger.success(f"{self.domain}\\{self.username} from certificate {self.mark_pwned()}") | ||
| else: | ||
| self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}") | ||
| self.db.add_credential("plaintext", domain, self.username, self.password) | ||
| # Prepare success credential text | ||
| self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}") | ||
|
|
||
| if self.username != "": | ||
| add_user_bh(self.username, self.domain, self.logger, self.config) | ||
|
|
@@ -517,6 +541,10 @@ def plaintext_login(self, domain, username, password): | |
| except OSError as e: | ||
| self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}") | ||
| return False | ||
| finally: | ||
| for tmp_file in (cert_file, key_file): | ||
| if tmp_file and os.path.exists(tmp_file): | ||
| os.remove(tmp_file) | ||
|
NeffIsBack marked this conversation as resolved.
|
||
|
|
||
| def hash_login(self, domain, username, ntlm_hash): | ||
| self.logger.extra["protocol"] = "LDAP" | ||
|
|
@@ -622,7 +650,7 @@ def check_if_admin(self): | |
| resp = self.search(search_filter, attributes, baseDN=self.baseDN) | ||
| resp_parsed = parse_result_attributes(resp) | ||
|
|
||
| if resp and (self.password != "" or self.lmhash != "" or self.nthash != "" or self.aesKey != "" or self.use_kcache) and self.username != "": | ||
| if resp and (self.password != "" or self.lmhash != "" or self.nthash != "" or self.aesKey != "" or self.use_kcache or self.args.schannel) and self.username != "": | ||
|
NeffIsBack marked this conversation as resolved.
|
||
| for item in resp_parsed: | ||
| self.sid_domain = "-".join(item["objectSid"].split("-")[:-1]) | ||
|
|
||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Uh oh!
There was an error while loading. Please reload this page.