NetExec/nxc/connection.py

475 lines
19 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
import random
import socket
from socket import AF_INET, AF_INET6, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME
from socket import getaddrinfo
2019-11-10 23:12:35 +00:00
from os.path import isfile
from threading import BoundedSemaphore
from functools import wraps
from time import sleep
from ipaddress import ip_address
from nxc.config import pwned_label
from nxc.helpers.logger import highlight
from nxc.logger import nxc_logger, NXCAdapter
from nxc.context import Context
from impacket.dcerpc.v5 import transport
sem = BoundedSemaphore(1)
global_failed_logins = 0
user_failed_logins = {}
2016-05-16 23:48:31 +00:00
2023-03-14 17:13:08 +00:00
def gethost_addrinfo(hostname):
try:
for res in getaddrinfo(hostname, None, AF_INET6, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
af, socktype, proto, canonname, sa = res
host = canonname if ip_address(sa[0]).is_link_local else sa[0]
except socket.gaierror:
for res in getaddrinfo(hostname, None, AF_INET, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
af, socktype, proto, canonname, sa = res
host = sa[0] if sa[0] else canonname
return host
def requires_admin(func):
def _decorator(self, *args, **kwargs):
2023-03-14 17:13:08 +00:00
if self.admin_privs is False:
return
return func(self, *args, **kwargs)
2023-05-02 15:17:59 +00:00
return wraps(func)(_decorator)
def dcom_FirewallChecker(iInterface, timeout):
stringBindings = iInterface.get_cinstance().get_string_bindings()
for strBinding in stringBindings:
if strBinding["wTowerId"] == 7:
if strBinding["aNetworkAddr"].find("[") >= 0:
binding, _, bindingPort = strBinding["aNetworkAddr"].partition("[")
bindingPort = "[" + bindingPort
else:
binding = strBinding["aNetworkAddr"]
bindingPort = ""
if binding.upper().find(iInterface.get_target().upper()) >= 0:
stringBinding = "ncacn_ip_tcp:" + strBinding["aNetworkAddr"][:-1]
break
elif iInterface.is_fqdn() and binding.upper().find(iInterface.get_target().upper().partition(".")[0]) >= 0:
2023-09-24 04:06:51 +00:00
stringBinding = f"ncacn_ip_tcp:{iInterface.get_target()}{bindingPort}"
if "stringBinding" not in locals():
return True, None
try:
rpctransport = transport.DCERPCTransportFactory(stringBinding)
rpctransport.set_connect_timeout(timeout)
rpctransport.connect()
rpctransport.disconnect()
except Exception as e:
nxc_logger.debug(f"Exception while connecting to {stringBinding}: {e}")
return False, stringBinding
else:
return True, stringBinding
2023-10-12 21:06:04 +00:00
class connection:
def __init__(self, args, db, host):
2023-03-14 17:13:08 +00:00
self.domain = None
2016-05-16 23:48:31 +00:00
self.args = args
self.db = db
self.hostname = host
self.conn = None
self.admin_privs = False
self.password = ""
self.username = ""
self.kerberos = True if self.args.kerberos or self.args.use_kcache or self.args.aesKey else False
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost
2023-02-07 11:06:42 +00:00
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
self.failed_logins = 0
self.local_ip = None
self.logger = nxc_logger
2023-02-21 20:01:42 +00:00
2017-04-14 21:26:17 +00:00
try:
self.host = gethost_addrinfo(self.hostname)
if self.args.kerberos:
self.host = self.hostname
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={ 'True' if self.args.kerberos else 'False' }")
2017-04-14 21:26:17 +00:00
except Exception as e:
self.logger.info(f"Error resolving hostname {self.hostname}: {e}")
2017-04-14 21:26:17 +00:00
return
if args.jitter:
jitter = args.jitter
if "-" in jitter:
start, end = jitter.split("-")
jitter = (int(start), int(end))
else:
jitter = (0, int(jitter))
value = random.choice(range(jitter[0], jitter[1]))
self.logger.debug(f"Doin' the jitterbug for {value} second(s)")
sleep(value)
try:
self.proto_flow()
except Exception as e:
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {e}")
@staticmethod
def proto_args(std_parser, module_parser):
return
def proto_logger(self):
pass
2016-05-16 23:48:31 +00:00
def enum_host_info(self):
return
2023-03-31 02:05:45 +00:00
def print_host_info(self):
return
def create_conn_obj(self):
return
def check_if_admin(self):
return
2023-05-02 15:17:59 +00:00
def kerberos_login(
self,
domain,
username,
password="",
ntlm_hash="",
aesKey="",
kdcHost="",
useCache=False,
):
return
def plaintext_login(self, domain, username, password):
return
def hash_login(self, domain, username, ntlm_hash):
return
def proto_flow(self):
2023-09-20 15:59:16 +00:00
self.logger.debug("Kicking off proto_flow")
self.proto_logger()
if self.create_conn_obj():
2021-10-17 15:50:29 +00:00
self.enum_host_info()
2022-02-10 21:36:07 +00:00
if self.print_host_info():
# because of null session
if self.login() or (self.username == "" and self.password == ""):
if hasattr(self.args, "module") and self.args.module:
2022-02-10 21:36:07 +00:00
self.call_modules()
else:
self.call_cmd_args()
def call_cmd_args(self):
"""Calls all the methods specified by the command line arguments
Iterates over the attributes of an object (self.args)
For each attribute, it checks if the object (self) has an attribute with the same name and if that attribute is callable (i.e., a function)
If both conditions are met and the attribute value is not False or None,
it calls the function and logs a debug message
2023-10-12 19:13:16 +00:00
Parameters
----------
self (object): The instance of the class.
2023-10-12 19:13:16 +00:00
Returns
-------
None
"""
for attr, value in vars(self.args).items():
if hasattr(self, attr) and callable(getattr(self, attr)):
if value is not False and value is not None:
self.logger.debug(f"Calling {attr}()")
getattr(self, attr)()
2016-05-16 23:48:31 +00:00
def call_modules(self):
"""Calls modules and performs various actions based on the module's attributes.
It iterates over the modules specified in the command line arguments.
For each module, it loads the module and creates a context object, then calls functions based on the module's attributes.
"""
for module in self.module:
2023-03-30 18:22:50 +00:00
self.logger.debug(f"Loading module {module.name} - {module}")
module_logger = NXCAdapter(
2023-03-30 18:22:50 +00:00
extra={
"module_name": module.name.upper(),
"host": self.host,
"port": self.args.port,
2023-05-02 15:17:59 +00:00
"hostname": self.hostname,
2023-03-30 18:22:50 +00:00
},
)
self.logger.debug(f"Loading context for module {module.name} - {module}")
context = Context(self.db, module_logger, self.args)
context.localip = self.local_ip
if hasattr(module, "on_request") or hasattr(module, "has_response"):
self.logger.debug(f"Module {module.name} has on_request or has_response methods")
self.server.connection = self
self.server.context.localip = self.local_ip
if hasattr(module, "on_login"):
self.logger.debug(f"Module {module.name} has on_login method")
module.on_login(context, self)
if self.admin_privs and hasattr(module, "on_admin_login"):
self.logger.debug(f"Module {module.name} has on_admin_login method")
module.on_admin_login(context, self)
if (not hasattr(module, "on_request") and not hasattr(module, "has_response")) and hasattr(module, "on_shutdown"):
self.logger.debug(f"Module {module.name} has on_shutdown method")
module.on_shutdown(context, self)
def inc_failed_login(self, username):
global global_failed_logins
global user_failed_logins
if username not in user_failed_logins.keys():
user_failed_logins[username] = 0
user_failed_logins[username] += 1
global_failed_logins += 1
self.failed_logins += 1
def over_fail_limit(self, username):
global global_failed_logins
global user_failed_logins
2023-03-14 17:13:08 +00:00
if global_failed_logins == self.args.gfail_limit:
return True
2023-03-14 17:13:08 +00:00
if self.failed_logins == self.args.fail_limit:
return True
if username in user_failed_logins.keys():
2023-03-14 17:13:08 +00:00
if self.args.ufail_limit == user_failed_logins[username]:
return True
return False
2023-04-21 09:44:17 +00:00
def query_db_creds(self):
"""Queries the database for credentials to be used for authentication.
2023-04-23 11:34:08 +00:00
Valid cred_id values are:
- a single cred_id
- a range specified with a dash (ex. 1-5)
- 'all' to select all credentials
:return: domains[], usernames[], owned[], secrets[], cred_types[]
2023-04-23 11:34:08 +00:00
"""
domains = []
usernames = []
2023-04-23 11:34:08 +00:00
owned = []
secrets = []
cred_types = []
2023-04-22 16:48:05 +00:00
creds = [] # list of tuples (cred_id, domain, username, secret, cred_type, pillaged_from) coming from the database
data = [] # Arbitrary data needed for the login, e.g. ssh_key
2023-04-21 09:44:17 +00:00
2023-04-22 16:48:05 +00:00
for cred_id in self.args.cred_id:
if cred_id.lower() == "all":
2023-04-22 16:48:05 +00:00
creds = self.db.get_credentials()
else:
if not self.db.get_credentials(filter_term=int(cred_id)):
self.logger.error(f"Invalid database credential ID {cred_id}!")
2023-04-22 16:48:05 +00:00
continue
creds.extend(self.db.get_credentials(filter_term=int(cred_id)))
2023-04-21 09:44:17 +00:00
for cred in creds:
c_id, domain, username, secret, cred_type, pillaged_from = cred
domains.append(domain)
usernames.append(username)
owned.append(False) # As these are likely valid we still want to test them if they are specified in the command line
secrets.append(secret)
cred_types.append(cred_type)
if len(secrets) != len(data):
data = [None] * len(secrets)
return domains, usernames, owned, secrets, cred_types, data
2023-04-21 16:58:08 +00:00
def parse_credentials(self):
r"""Parse credentials from the command line or from a file specified.
Usernames can be specified with a domain (domain\\username) or without (username).
If the file contains domain\\username the domain specified will be overwritten by the one in the file.
2023-04-23 11:34:08 +00:00
:return: domain[], username[], owned[], secret[], cred_type[]
2023-04-21 16:58:08 +00:00
"""
domain = []
username = []
2023-04-23 11:34:08 +00:00
owned = []
2023-04-21 16:58:08 +00:00
secret = []
cred_type = []
# Parse usernames
for user in self.args.username:
if isfile(user):
2023-10-12 21:06:04 +00:00
with open(user) as user_file:
2023-04-21 16:58:08 +00:00
for line in user_file:
if "\\" in line:
domain_single, username_single = line.split("\\")
else:
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
2023-04-21 16:58:08 +00:00
username_single = line
2023-07-04 11:25:42 +00:00
domain.append(domain_single)
username.append(username_single.strip())
owned.append(False)
2023-04-21 16:58:08 +00:00
else:
if "\\" in user:
domain_single, username_single = user.split("\\")
else:
2023-05-02 10:45:30 +00:00
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
2023-04-21 16:58:08 +00:00
username_single = user
2023-07-04 11:25:42 +00:00
domain.append(domain_single)
username.append(username_single)
owned.append(False)
2023-04-21 16:58:08 +00:00
# Parse passwords
for password in self.args.password:
if isfile(password):
2023-09-20 14:01:23 +00:00
try:
2023-10-12 21:06:04 +00:00
with open(password, errors=("ignore" if self.args.ignore_pw_decoding else "strict")) as password_file:
for line in password_file:
secret.append(line.strip())
cred_type.append("plaintext")
2023-09-20 14:01:23 +00:00
except UnicodeDecodeError as e:
self.logger.error(f"{type(e).__name__}: Could not decode password file. Make sure the file only contains UTF-8 characters.")
2023-09-21 10:36:04 +00:00
self.logger.error("You can ignore non UTF-8 characters with the option '--ignore-pw-decoding'")
2023-09-20 14:01:23 +00:00
exit(1)
2023-04-21 16:58:08 +00:00
else:
secret.append(password)
cred_type.append("plaintext")
2023-04-21 16:58:08 +00:00
# Parse NTLM-hashes
2023-05-02 10:45:30 +00:00
if hasattr(self.args, "hash") and self.args.hash:
2023-04-21 16:58:08 +00:00
for ntlm_hash in self.args.hash:
if isfile(ntlm_hash):
2023-10-12 21:06:04 +00:00
with open(ntlm_hash) as ntlm_hash_file:
2023-04-21 16:58:08 +00:00
for line in ntlm_hash_file:
secret.append(line.strip())
cred_type.append("hash")
2023-04-21 16:58:08 +00:00
else:
secret.append(ntlm_hash)
cred_type.append("hash")
# Parse AES keys
if self.args.aesKey:
for aesKey in self.args.aesKey:
if isfile(aesKey):
2023-10-12 21:06:04 +00:00
with open(aesKey) as aesKey_file:
for line in aesKey_file:
secret.append(line.strip())
cred_type.append("aesKey")
else:
secret.append(aesKey)
cred_type.append("aesKey")
2023-04-21 16:58:08 +00:00
# Allow trying multiple users with a single password
if len(username) > 1 and len(secret) == 1:
secret = secret * len(username)
cred_type = cred_type * len(username)
self.args.no_bruteforce = True
return domain, username, owned, secret, cred_type, [None] * len(secret)
2023-04-21 16:58:08 +00:00
def try_credentials(self, domain, username, owned, secret, cred_type, data=None):
"""Try to login using the specified credentials and protocol.
2023-04-23 11:34:08 +00:00
Possible login methods are:
- plaintext (/kerberos)
- NTLM-hash (/kerberos)
- AES-key
"""
if self.over_fail_limit(username):
return False
2023-04-23 11:34:08 +00:00
if self.args.continue_on_success and owned:
return False
2023-07-12 20:14:59 +00:00
# Enforcing FQDN for SMB if not using local authentication. Related issues/PRs: #26, #28, #24, #38
if self.args.protocol == "smb" and not self.args.local_auth and "." not in domain and not self.args.laps and secret != "" and not (self.domain.upper() == self.hostname.upper()):
2023-07-04 11:25:42 +00:00
self.logger.error(f"Domain {domain} for user {username.rstrip()} need to be FQDN ex:domain.local, not domain")
return False
with sem:
if cred_type == "plaintext":
if self.args.kerberos:
return self.kerberos_login(domain, username, secret, "", "", self.kdcHost, False)
elif hasattr(self.args, "domain"): # Some protocolls don't use domain for login
2023-05-02 10:45:30 +00:00
return self.plaintext_login(domain, username, secret)
elif self.args.protocol == "ssh":
return self.plaintext_login(username, secret, data)
2023-05-02 10:45:30 +00:00
else:
return self.plaintext_login(username, secret)
elif cred_type == "hash":
if self.args.kerberos:
return self.kerberos_login(domain, username, "", secret, "", self.kdcHost, False)
return self.hash_login(domain, username, secret)
elif cred_type == "aesKey":
return self.kerberos_login(domain, username, "", "", secret, self.kdcHost, False)
2023-04-21 09:44:17 +00:00
def login(self):
"""Try to login using the credentials specified in the command line or in the database.
2023-04-23 11:34:08 +00:00
:return: True if the login was successful and "--continue-on-success" was not specified, False otherwise.
"""
# domain[n] always corresponds to username[n] and owned [n]
2023-04-21 09:44:17 +00:00
domain = []
username = []
2023-04-23 11:34:08 +00:00
owned = [] # Determines whether we have found a valid credential for this user. Default: False
# secret[n] always corresponds to cred_type[n]
2023-04-21 09:44:17 +00:00
secret = []
cred_type = []
data = [] # Arbitrary data needed for the login, e.g. ssh_key
2023-04-21 09:44:17 +00:00
if self.args.cred_id:
db_domain, db_username, db_owned, db_secret, db_cred_type, db_data = self.query_db_creds()
2023-04-21 16:58:08 +00:00
domain.extend(db_domain)
username.extend(db_username)
2023-04-23 12:07:02 +00:00
owned.extend(db_owned)
2023-04-21 16:58:08 +00:00
secret.extend(db_secret)
cred_type.extend(db_cred_type)
data.extend(db_data)
2023-04-21 16:58:08 +00:00
if self.args.username:
parsed_domain, parsed_username, parsed_owned, parsed_secret, parsed_cred_type, parsed_data = self.parse_credentials()
2023-04-21 16:58:08 +00:00
domain.extend(parsed_domain)
username.extend(parsed_username)
2023-04-23 12:07:02 +00:00
owned.extend(parsed_owned)
2023-04-21 16:58:08 +00:00
secret.extend(parsed_secret)
cred_type.extend(parsed_cred_type)
data.extend(parsed_data)
2023-04-21 09:44:17 +00:00
2022-10-20 16:11:10 +00:00
if self.args.use_kcache:
2022-10-20 16:08:30 +00:00
with sem:
username = self.args.username[0] if len(self.args.username) else ""
password = self.args.password[0] if len(self.args.password) else ""
self.kerberos_login(self.domain, username, password, "", "", self.kdcHost, True)
2023-04-21 16:58:08 +00:00
self.logger.info("Successfully authenticated using Kerberos cache")
2023-02-01 11:04:13 +00:00
return True
2023-04-21 16:58:08 +00:00
if not self.args.no_bruteforce:
for secr_index, secr in enumerate(secret):
for user_index, user in enumerate(username):
if self.try_credentials(domain[user_index], user, owned[user_index], secr, cred_type[secr_index], data[secr_index]):
2023-04-23 12:07:02 +00:00
owned[user_index] = True
if not self.args.continue_on_success:
return True
else:
if len(username) != len(secret):
self.logger.error("Number provided of usernames and passwords/hashes do not match!")
return False
for user_index, user in enumerate(username):
if self.try_credentials(domain[user_index], user, owned[user_index], secret[user_index], cred_type[user_index], data[user_index]) and not self.args.continue_on_success:
2023-04-23 12:07:02 +00:00
owned[user_index] = True
if not self.args.continue_on_success:
return True
def mark_pwned(self):
return highlight(f"({pwned_label})" if self.admin_privs else "")