mirror of
https://github.com/Pennyw0rth/NetExec
synced 2026-06-06 16:34:30 +00:00
7f65c0eccb
This change allows the program to return the name of the user being authenticated when using Kerberos with the protocol mssql.
415 lines
19 KiB
Python
Executable File
415 lines
19 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import socket
|
|
import logging
|
|
from cme.logger import CMEAdapter
|
|
from io import StringIO
|
|
from cme.protocols.mssql.mssqlexec import MSSQLEXEC
|
|
from cme.connection import *
|
|
from cme.helpers.logger import highlight
|
|
from cme.helpers.bloodhound import add_user_bh
|
|
from cme.helpers.powershell import create_ps_command
|
|
from impacket import tds
|
|
import configparser
|
|
from impacket.krb5.ccache import CCache
|
|
from impacket.smbconnection import SMBConnection, SessionError
|
|
from impacket.tds import SQLErrorException, TDS_LOGINACK_TOKEN, TDS_ERROR_TOKEN, TDS_ENVCHANGE_TOKEN, TDS_INFO_TOKEN, \
|
|
TDS_ENVCHANGE_VARCHAR, TDS_ENVCHANGE_DATABASE, TDS_ENVCHANGE_LANGUAGE, TDS_ENVCHANGE_CHARSET, TDS_ENVCHANGE_PACKETSIZE
|
|
|
|
|
|
class mssql(connection):
|
|
def __init__(self, args, db, host):
|
|
self.mssql_instances = None
|
|
self.domain = None
|
|
self.server_os = None
|
|
self.hash = None
|
|
self.os_arch = None
|
|
self.nthash = ''
|
|
|
|
connection.__init__(self, args, db, host)
|
|
|
|
@staticmethod
|
|
def proto_args(parser, std_parser, module_parser):
|
|
mssql_parser = parser.add_parser('mssql', help="own stuff using MSSQL", parents=[std_parser, module_parser])
|
|
dgroup = mssql_parser.add_mutually_exclusive_group()
|
|
dgroup.add_argument("-d", metavar="DOMAIN", dest='domain', type=str, help="domain name")
|
|
dgroup.add_argument("--local-auth", action='store_true', help='authenticate locally to each target')
|
|
mssql_parser.add_argument("-H", '--hash', metavar="HASH", dest='hash', nargs='+', default=[], help='NTLM hash(es) or file(s) containing NTLM hashes')
|
|
mssql_parser.add_argument("--port", default=1433, type=int, metavar='PORT', help='MSSQL port (default: 1433)')
|
|
mssql_parser.add_argument("-q", "--query", dest='mssql_query', metavar='QUERY', type=str, help='execute the specified query against the MSSQL DB')
|
|
mssql_parser.add_argument("--no-bruteforce", action='store_true', help='No spray when using file for username and password (user1 => password1, user2 => password2')
|
|
mssql_parser.add_argument("--continue-on-success", action='store_true', help="continues authentication attempts even after successes")
|
|
|
|
cgroup = mssql_parser.add_argument_group("Command Execution", "options for executing commands")
|
|
cgroup.add_argument('--force-ps32', action='store_true', help='force the PowerShell command to run in a 32-bit process')
|
|
cgroup.add_argument('--no-output', action='store_true', help='do not retrieve command output')
|
|
xgroup = cgroup.add_mutually_exclusive_group()
|
|
xgroup.add_argument("-x", metavar="COMMAND", dest='execute', help="execute the specified command")
|
|
xgroup.add_argument("-X", metavar="PS_COMMAND", dest='ps_execute', help='execute the specified PowerShell command')
|
|
|
|
psgroup = mssql_parser.add_argument_group('Powershell Obfuscation', "Options for PowerShell script obfuscation")
|
|
psgroup.add_argument('--obfs', action='store_true', help='Obfuscate PowerShell scripts')
|
|
psgroup.add_argument('--clear-obfscripts', action='store_true', help='Clear all cached obfuscated PowerShell scripts')
|
|
|
|
tgroup = mssql_parser.add_argument_group("Files", "Options for put and get remote files")
|
|
tgroup.add_argument("--put-file", nargs=2, metavar="FILE", help='Put a local file into remote target, ex: whoami.txt C:\\Windows\\Temp\\whoami.txt')
|
|
tgroup.add_argument("--get-file", nargs=2, metavar="FILE", help='Get a remote file, ex: C:\\Windows\\Temp\\whoami.txt whoami.txt')
|
|
|
|
return parser
|
|
|
|
def proto_flow(self):
|
|
self.proto_logger()
|
|
if self.create_conn_obj():
|
|
self.enum_host_info()
|
|
self.print_host_info()
|
|
self.login()
|
|
if hasattr(self.args, 'module') and self.args.module:
|
|
self.call_modules()
|
|
else:
|
|
self.call_cmd_args()
|
|
|
|
def proto_logger(self):
|
|
self.logger = CMEAdapter(extra={
|
|
'protocol': 'MSSQL',
|
|
'host': self.host,
|
|
'port': self.args.port,
|
|
'hostname': 'None'
|
|
})
|
|
|
|
def enum_host_info(self):
|
|
# this try pass breaks module http server, more info https://github.com/byt3bl33d3r/CrackMapExec/issues/363
|
|
try:
|
|
# Probably a better way of doing this, grab our IP from the socket
|
|
self.local_ip = str(self.conn.socket).split()[2].split('=')[1].split(':')[0]
|
|
except:
|
|
pass
|
|
|
|
if self.args.domain:
|
|
self.domain = self.args.domain
|
|
else:
|
|
try:
|
|
smb_conn = SMBConnection(self.host, self.host, None)
|
|
try:
|
|
smb_conn.login('', '')
|
|
except SessionError as e:
|
|
if "STATUS_ACCESS_DENIED" in e.getErrorString():
|
|
pass
|
|
|
|
self.domain = smb_conn.getServerDNSDomainName()
|
|
self.hostname = smb_conn.getServerName()
|
|
self.server_os = smb_conn.getServerOS()
|
|
self.logger.extra['hostname'] = self.hostname
|
|
|
|
try:
|
|
smb_conn.logoff()
|
|
except:
|
|
pass
|
|
|
|
if self.args.domain:
|
|
self.domain = self.args.domain
|
|
|
|
if self.args.local_auth:
|
|
self.domain = self.hostname
|
|
|
|
except Exception as e:
|
|
self.logger.error("Error retrieving host domain: {} specify one manually with the '-d' flag".format(e))
|
|
|
|
self.mssql_instances = self.conn.getInstances(0)
|
|
self.db.add_computer(self.host, self.hostname, self.domain, self.server_os, len(self.mssql_instances))
|
|
|
|
try:
|
|
self.conn.disconnect()
|
|
except:
|
|
pass
|
|
|
|
def print_host_info(self):
|
|
self.logger.info(u"{} (name:{}) (domain:{})".format(self.server_os,
|
|
self.hostname,
|
|
self.domain))
|
|
# if len(self.mssql_instances) > 0:
|
|
# self.logger.info("MSSQL DB Instances: {}".format(len(self.mssql_instances)))
|
|
# for i, instance in enumerate(self.mssql_instances):
|
|
# self.logger.debug("Instance {}".format(i))
|
|
# for key in instance.keys():
|
|
# self.logger.debug(key + ":" + instance[key])
|
|
|
|
def create_conn_obj(self):
|
|
try:
|
|
self.conn = tds.MSSQL(self.host, self.args.port, rowsPrinter=self.logger)
|
|
self.conn.connect()
|
|
except socket.error:
|
|
return False
|
|
|
|
return True
|
|
|
|
def check_if_admin(self):
|
|
try:
|
|
self.conn.sql_query("SELECT IS_SRVROLEMEMBER('sysadmin')")
|
|
self.conn.printRows()
|
|
query_output = self.conn._MSSQL__rowsPrinter.getMessage()
|
|
query_output = query_output.strip("\n-")
|
|
|
|
if int(query_output):
|
|
self.admin_privs = True
|
|
else:
|
|
return False
|
|
except Exception as e:
|
|
self.logger.error('Error calling check_if_admin(): {}'.format(e))
|
|
return False
|
|
|
|
return True
|
|
|
|
def kerberos_login(self, domain, username, password = '', ntlm_hash = '', aesKey = '', kdcHost = '', useCache = False):
|
|
try:
|
|
self.conn.disconnect()
|
|
except:
|
|
pass
|
|
self.create_conn_obj()
|
|
logging.getLogger("impacket").disabled = True
|
|
|
|
nthash = ''
|
|
hashes = None
|
|
if ntlm_hash != '':
|
|
if ntlm_hash.find(':') != -1:
|
|
hashes = ntlm_hash
|
|
nthash = ntlm_hash.split(':')[1]
|
|
else:
|
|
# only nt hash
|
|
hashes = ':%s' % ntlm_hash
|
|
nthash = ntlm_hash
|
|
|
|
if not all('' == s for s in [self.nthash, password, aesKey]):
|
|
kerb_pass = next(s for s in [self.nthash, password, aesKey] if s)
|
|
else:
|
|
kerb_pass = ''
|
|
try:
|
|
res = self.conn.kerberosLogin(None, username, password, domain, hashes, aesKey, kdcHost=kdcHost, useCache=useCache)
|
|
if res is not True:
|
|
self.conn.printReplies()
|
|
return False
|
|
|
|
self.password = password
|
|
if username == '' and useCache:
|
|
ccache = CCache.loadFile(os.getenv('KRB5CCNAME'))
|
|
principal = ccache.principal.toPrincipal()
|
|
self.username = principal.components[0]
|
|
username = principal.components[0]
|
|
else:
|
|
self.username = username
|
|
self.domain = domain
|
|
self.check_if_admin()
|
|
|
|
out = u'{}{}{} {}'.format('{}\\'.format(domain) if not self.args.local_auth else '',
|
|
username,
|
|
# Show what was used between cleartext, nthash, aesKey and ccache
|
|
" from ccache" if useCache
|
|
else ":%s" % (kerb_pass if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8),
|
|
highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))
|
|
self.logger.success(out)
|
|
if not self.args.local_auth:
|
|
add_user_bh(self.username, self.domain, self.logger, self.config)
|
|
if not self.args.continue_on_success:
|
|
return True
|
|
except Exception as e:
|
|
self.logger.error(u'{}\\{}{} {}'.format('{}\\'.format(domain) if not self.args.local_auth else '',
|
|
username,
|
|
# Show what was used between cleartext, nthash, aesKey and ccache
|
|
" from ccache" if useCache
|
|
else ":%s" % (kerb_pass if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8),
|
|
e))
|
|
return False
|
|
|
|
def plaintext_login(self, domain, username, password):
|
|
try:
|
|
self.conn.disconnect()
|
|
except:
|
|
pass
|
|
self.create_conn_obj()
|
|
|
|
try:
|
|
res = self.conn.login(None, username, password, domain, None, not self.args.local_auth)
|
|
if res is not True:
|
|
self.conn.printReplies()
|
|
return False
|
|
|
|
self.password = password
|
|
self.username = username
|
|
self.domain = domain
|
|
self.check_if_admin()
|
|
self.db.add_credential('plaintext', domain, username, password)
|
|
|
|
if self.admin_privs:
|
|
self.db.add_admin_user('plaintext', domain, username, password, self.host)
|
|
|
|
out = u'{}{}:{} {}'.format('{}\\'.format(domain) if not self.args.local_auth else '',
|
|
username,
|
|
password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8,
|
|
highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))
|
|
self.logger.success(out)
|
|
if not self.args.local_auth:
|
|
add_user_bh(self.username, self.domain, self.logger, self.config)
|
|
if not self.args.continue_on_success:
|
|
return True
|
|
except Exception as e:
|
|
self.logger.error(u'{}\\{}:{} {}'.format(domain,
|
|
username,
|
|
password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8,
|
|
e))
|
|
return False
|
|
|
|
def hash_login(self, domain, username, ntlm_hash):
|
|
lmhash = ''
|
|
nthash = ''
|
|
|
|
# This checks to see if we didn't provide the LM Hash
|
|
if ntlm_hash.find(':') != -1:
|
|
lmhash, nthash = ntlm_hash.split(':')
|
|
else:
|
|
nthash = ntlm_hash
|
|
|
|
try:
|
|
self.conn.disconnect()
|
|
except:
|
|
pass
|
|
self.create_conn_obj()
|
|
|
|
try:
|
|
res = self.conn.login(None, username, '', domain, ':' + nthash if not lmhash else ntlm_hash, not self.args.local_auth)
|
|
if res is not True:
|
|
self.conn.printReplies()
|
|
return False
|
|
|
|
self.hash = ntlm_hash
|
|
self.username = username
|
|
self.domain = domain
|
|
self.check_if_admin()
|
|
self.db.add_credential('hash', domain, username, ntlm_hash)
|
|
|
|
if self.admin_privs:
|
|
self.db.add_admin_user('hash', domain, username, ntlm_hash, self.host)
|
|
|
|
out = u'{}\\{} {} {}'.format(domain,
|
|
username,
|
|
ntlm_hash if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8,
|
|
highlight('({})'.format(self.config.get('CME', 'pwn3d_label')) if self.admin_privs else ''))
|
|
self.logger.success(out)
|
|
if not self.args.local_auth:
|
|
add_user_bh(self.username, self.domain, self.logger, self.config)
|
|
if not self.args.continue_on_success:
|
|
return True
|
|
except Exception as e:
|
|
self.logger.error(u'{}\\{}:{} {}'.format(domain,
|
|
username,
|
|
ntlm_hash if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8,
|
|
e))
|
|
return False
|
|
|
|
def mssql_query(self):
|
|
self.conn.sql_query(self.args.mssql_query)
|
|
self.conn.printRows()
|
|
for line in StringIO(self.conn._MSSQL__rowsPrinter.getMessage()).readlines():
|
|
if line.strip() != '':
|
|
self.logger.highlight(line.strip())
|
|
return self.conn._MSSQL__rowsPrinter.getMessage()
|
|
|
|
@requires_admin
|
|
def execute(self, payload=None, get_output=False, methods=None):
|
|
if not payload and self.args.execute:
|
|
payload = self.args.execute
|
|
if not self.args.no_output: get_output = True
|
|
|
|
logging.debug('Command to execute:\n{}'.format(payload))
|
|
exec_method = MSSQLEXEC(self.conn)
|
|
raw_output = exec_method.execute(payload, get_output)
|
|
logging.debug('Executed command via mssqlexec')
|
|
|
|
if hasattr(self, 'server'): self.server.track_host(self.host)
|
|
|
|
output = u'{}'.format(raw_output)
|
|
|
|
if self.args.execute or self.args.ps_execute:
|
|
#self.logger.success('Executed command {}'.format('via {}'.format(self.args.exec_method) if self.args.exec_method else ''))
|
|
self.logger.success('Executed command via mssqlexec')
|
|
buf = StringIO(output).readlines()
|
|
for line in buf:
|
|
if line.strip() != '':
|
|
self.logger.highlight(line.strip())
|
|
|
|
return output
|
|
|
|
@requires_admin
|
|
def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, dont_obfs=True):
|
|
if not payload and self.args.ps_execute:
|
|
payload = self.args.ps_execute
|
|
if not self.args.no_output: get_output = True
|
|
|
|
# We're disabling PS obfuscation by default as it breaks the MSSQLEXEC execution method (probably an escaping issue)
|
|
ps_command = create_ps_command(payload, force_ps32=force_ps32, dont_obfs=dont_obfs)
|
|
return self.execute(ps_command, get_output)
|
|
|
|
@requires_admin
|
|
def put_file(self):
|
|
self.logger.info('Copy {} to {}'.format(self.args.put_file[0], self.args.put_file[1]))
|
|
with open(self.args.put_file[0], 'rb') as f:
|
|
try:
|
|
data = f.read()
|
|
self.logger.info('Size is {} bytes'.format(len(data)))
|
|
exec_method = MSSQLEXEC(self.conn)
|
|
exec_method.put_file(data, self.args.put_file[1])
|
|
if exec_method.file_exists(self.args.put_file[1]):
|
|
self.logger.success('File has been uploaded on the remote machine')
|
|
else:
|
|
self.logger.error('File does not exist on the remote system.. erorr during upload')
|
|
except Exception as e:
|
|
self.logger.error('Error during upload : {}'.format(e))
|
|
|
|
@requires_admin
|
|
def get_file(self):
|
|
self.logger.info('Copy {} to {}'.format(self.args.get_file[0], self.args.get_file[1]))
|
|
try:
|
|
exec_method = MSSQLEXEC(self.conn)
|
|
exec_method.get_file(self.args.get_file[0], self.args.get_file[1])
|
|
self.logger.success('File {} was transferred to {}'.format(self.args.get_file[0], self.args.get_file[1]))
|
|
except Exception as e:
|
|
self.logger.error('Error reading file {}: {}'.format(self.args.get_file[0], e))
|
|
|
|
# We hook these functions in the tds library to use CME's logger instead of printing the output to stdout
|
|
# The whole tds library in impacket needs a good overhaul to preserve my sanity
|
|
|
|
|
|
def printRepliesCME(self):
|
|
for keys in self.replies.keys():
|
|
for i, key in enumerate(self.replies[keys]):
|
|
if key['TokenType'] == TDS_ERROR_TOKEN:
|
|
error = "ERROR(%s): Line %d: %s" % (key['ServerName'].decode('utf-16le'), key['LineNumber'], key['MsgText'].decode('utf-16le'))
|
|
self.lastError = SQLErrorException("ERROR: Line %d: %s" % (key['LineNumber'], key['MsgText'].decode('utf-16le')))
|
|
self._MSSQL__rowsPrinter.error(error)
|
|
|
|
elif key['TokenType'] == TDS_INFO_TOKEN:
|
|
self._MSSQL__rowsPrinter.info("INFO(%s): Line %d: %s" % (key['ServerName'].decode('utf-16le'), key['LineNumber'], key['MsgText'].decode('utf-16le')))
|
|
|
|
elif key['TokenType'] == TDS_LOGINACK_TOKEN:
|
|
self._MSSQL__rowsPrinter.info("ACK: Result: %s - %s (%d%d %d%d) " % (key['Interface'], key['ProgName'].decode('utf-16le'), key['MajorVer'], key['MinorVer'], key['BuildNumHi'], key['BuildNumLow']))
|
|
|
|
elif key['TokenType'] == TDS_ENVCHANGE_TOKEN:
|
|
if key['Type'] in (TDS_ENVCHANGE_DATABASE, TDS_ENVCHANGE_LANGUAGE, TDS_ENVCHANGE_CHARSET, TDS_ENVCHANGE_PACKETSIZE):
|
|
record = TDS_ENVCHANGE_VARCHAR(key['Data'])
|
|
if record['OldValue'] == '':
|
|
record['OldValue'] = 'None'.encode('utf-16le')
|
|
elif record['NewValue'] == '':
|
|
record['NewValue'] = 'None'.encode('utf-16le')
|
|
if key['Type'] == TDS_ENVCHANGE_DATABASE:
|
|
_type = 'DATABASE'
|
|
elif key['Type'] == TDS_ENVCHANGE_LANGUAGE:
|
|
_type = 'LANGUAGE'
|
|
elif key['Type'] == TDS_ENVCHANGE_CHARSET:
|
|
_type = 'CHARSET'
|
|
elif key['Type'] == TDS_ENVCHANGE_PACKETSIZE:
|
|
_type = 'PACKETSIZE'
|
|
else:
|
|
_type = "%d" % key['Type']
|
|
self._MSSQL__rowsPrinter.info("ENVCHANGE(%s): Old Value: %s, New Value: %s" % (_type,record['OldValue'].decode('utf-16le'), record['NewValue'].decode('utf-16le')))
|
|
|
|
tds.MSSQL.printReplies = printRepliesCME
|