NetExec/nxc/connection.py

452 lines
18 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
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:
2023-05-09 08:26:01 +00:00
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:
2023-05-09 08:26:01 +00:00
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:
stringBinding = 'ncacn_ip_tcp:%s%s' % (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:
return False, stringBinding
else:
return True, stringBinding
2017-03-27 21:09:36 +00:00
class connection(object):
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):
self.logger.debug(f"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):
2019-11-10 23:12:35 +00:00
for k, v in vars(self.args).items():
if hasattr(self, k) and hasattr(getattr(self, k), "__call__"):
if v is not False and v is not None:
self.logger.debug(f"Calling {k}()")
r = getattr(self, k)()
2016-05-16 23:48:31 +00:00
def call_modules(self):
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):
2023-04-23 11:34:08 +00:00
"""
Queries the database for credentials to be used for authentication.
Valid cred_id values are:
- a single cred_id
- a range specified with a dash (ex. 1-5)
- 'all' to select all credentials
:return: domain[], username[], owned[], secret[], cred_type[]
"""
2023-04-21 09:44:17 +00:00
domain = []
username = []
2023-04-23 11:34:08 +00:00
owned = []
2023-04-21 09:44:17 +00:00
secret = []
cred_type = []
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 isinstance(cred_id, str) and cred_id.lower() == 'all':
creds = self.db.get_credentials()
else:
if not self.db.get_credentials(filter_term=int(cred_id)):
self.logger.error('Invalid database credential ID {}!'.format(cred_id))
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_single, username_single, secret_single, cred_type_single, pillaged_from = cred
domain.append(domain_single)
username.append(username_single)
owned.append(False) # As these are likely valid we still want to test them if they are specified in the command line
2023-04-21 09:44:17 +00:00
secret.append(secret_single)
cred_type.append(cred_type_single)
2023-07-14 20:23:03 +00:00
if len(secret) != len(data): data = [None] * len(secret)
return domain, username, owned, secret, cred_type, data
2023-04-21 16:58:08 +00:00
def parse_credentials(self):
"""
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):
with open(user, 'r') as user_file:
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-09-21 10:36:04 +00:00
with open(password, 'r', 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')
# 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):
with open(ntlm_hash, 'r') as ntlm_hash_file:
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):
with open(aesKey, 'r') 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):
2023-04-23 11:34:08 +00:00
"""
Try to login using the specified credentials and protocol.
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):
2023-04-23 11:34:08 +00:00
"""
Try to login using the credentials specified in the command line or in the database.
: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 "")