Files
2022-12-26 02:22:25 +01:00

616 lines
30 KiB
Python

#!/usr/bin/env python3
# Impacket - Collection of Python classes for working with network protocols.
#
# Copyright (C) 2022 Fortra. All rights reserved.
#
# This software is provided under a slightly modified version
# of the Apache Software License. See the accompanying LICENSE file
# for more information.
#
# Description:
# Terminal Services manipulation tool.
# Initial idea was to provide similar functionality as the QWINSTA and other TS* windows commands:
#
# qwinsta: Display information about Remote Desktop Services sessions.
# tasklist: Display a list of currently running processes on the system.
# taskkill: Terminate tasks by process id (PID) or image name
# tscon: Attaches a user session to a remote desktop session
# tsdiscon: Disconnects a Remote Desktop Services session
# tslogoff: Signs-out a Remote Desktop Services session
# shutdown: Remote shutdown
# msg: Send a message to Remote Desktop Services session (MSGBOX)
#
# Author:
# Alexander Korznikov (@nopernik)
#
# Reference for:
# [MS-TSTS]
#
import argparse
import codecs
import logging
import sys
from struct import unpack
from impacket import version
from impacket.examples import logger
from impacket.examples.utils import parse_target
from impacket.smbconnection import SMBConnection
from impacket import LOG
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from impacket.dcerpc.v5 import tsts as TSTS
import traceback
class TSHandler:
def __init__(self, username, password, domain, options):
self.__username = username
self.__password = password
self.__domain = domain
self.__options = options
self.__action = options.action.lower()
self.__lmhash = ''
self.__nthash = ''
self.__aesKey = options.aesKey
self.__doKerberos = options.k
self.__kdcHost = options.dc_ip
self.__smbConnection = None
self.__remoteOps = None
if options.hashes is not None:
self.__lmhash, self.__nthash = options.hashes.split(':')
def connect(self, remoteName, remoteHost):
self.__smbConnection = SMBConnection(remoteName, remoteHost, sess_port=int(self.__options.port))
if self.__doKerberos:
self.__smbConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash,
self.__nthash, self.__aesKey, self.__kdcHost)
else:
self.__smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
def run(self, remoteName, remoteHost):
if self.__options.action == 'shutdown':
if not max([options.logoff, options.shutdown, options.reboot, options.poweroff]):
LOG.error('At least one flag is required: -logoff, -shutdown, -reboot or -poweroff')
exit(1)
self.connect(remoteName, remoteHost)
getattr(self,'do_'+self.__action)()
def get_session_list(self):
# Retreive session list
smb = self.__smbConnection
target_ip = self.__options.target_ip
with TSTS.TermSrvEnumeration(self.__smbConnection, self.__options.target_ip) as lsm:
handle = lsm.hRpcOpenEnum()
rsessions = lsm.hRpcGetEnumResult(handle, Level=1)['ppSessionEnumResult']
lsm.hRpcCloseEnum(handle)
self.sessions = {}
for i in rsessions:
sess = i['SessionInfo']['SessionEnum_Level1']
state = TSTS.enum2value(TSTS.WINSTATIONSTATECLASS, sess['State']).split('_')[-1]
self.sessions[sess['SessionId']] = { 'state' :state,
'SessionName' :sess['Name'],
'RemoteIp' :'',
'ClientName' :'',
'Username' :'',
'Domain' :'',
'Resolution' :'',
'ClientTimeZone':''
}
def enumerate_sessions_config(self):
# Get session config one by one
smb = self.__smbConnection
target_ip = self.__options.target_ip
if len(self.sessions):
with TSTS.RCMPublic(self.__smbConnection, self.__options.target_ip) as termsrv:
for SessionId in self.sessions:
resp = termsrv.hRpcGetClientData(SessionId)
if resp is not None:
self.sessions[SessionId]['RemoteIp'] = resp['ppBuff']['ClientAddress']
self.sessions[SessionId]['ClientName'] = resp['ppBuff']['ClientName']
if len(resp['ppBuff']['UserName']) and not len(self.sessions[SessionId]['Username']):
self.sessions[SessionId]['Username'] = resp['ppBuff']['UserName']
if len(resp['ppBuff']['Domain']) and not len(self.sessions[SessionId]['Domain']):
self.sessions[SessionId]['Domain'] = resp['ppBuff']['Domain']
self.sessions[SessionId]['Resolution'] = '{}x{}'.format(
resp['ppBuff']['HRes'],
resp['ppBuff']['VRes']
)
self.sessions[SessionId]['ClientTimeZone'] = resp['ppBuff']['ClientTimeZone']['StandardName']
def enumerate_sessions_info(self):
# Get session info one by one
smb = self.__smbConnection
target_ip = self.__options.target_ip
if len(self.sessions):
with TSTS.TermSrvSession(self.__smbConnection, self.__options.target_ip) as TermSrvSession:
for SessionId in self.sessions.keys():
sessdata = TermSrvSession.hRpcGetSessionInformationEx(SessionId)
sessflags = TSTS.enum2value(TSTS.SESSIONFLAGS, sessdata['LSMSessionInfoExPtr']['LSM_SessionInfo_Level1']['SessionFlags'])
self.sessions[SessionId]['flags'] = sessflags
domain = sessdata['LSMSessionInfoExPtr']['LSM_SessionInfo_Level1']['DomainName']
if not len(self.sessions[SessionId]['Domain']) and len(domain):
self.sessions[SessionId]['Domain'] = domain
username = sessdata['LSMSessionInfoExPtr']['LSM_SessionInfo_Level1']['UserName']
if not len(self.sessions[SessionId]['Username']) and len(username):
self.sessions[SessionId]['Username'] = username
self.sessions[SessionId]['ConnectTime'] = sessdata['LSMSessionInfoExPtr']['LSM_SessionInfo_Level1']['ConnectTime']
self.sessions[SessionId]['DisconnectTime'] = sessdata['LSMSessionInfoExPtr']['LSM_SessionInfo_Level1']['DisconnectTime']
self.sessions[SessionId]['LogonTime'] = sessdata['LSMSessionInfoExPtr']['LSM_SessionInfo_Level1']['LogonTime']
self.sessions[SessionId]['LastInputTime'] = sessdata['LSMSessionInfoExPtr']['LSM_SessionInfo_Level1']['LastInputTime']
def do_qwinsta(self):
options = self.__options
desktop_states = {
'WTS_SESSIONSTATE_UNKNOWN': '',
'WTS_SESSIONSTATE_LOCK' : 'Locked',
'WTS_SESSIONSTATE_UNLOCK' : 'Unlocked',
}
self.get_session_list()
if not len(self.sessions):
print('No sessions found...')
return
self.enumerate_sessions_info()
if options.verbose:
self.enumerate_sessions_config()
maxSessionNameLen = max([len(self.sessions[i]['SessionName'])+1 for i in self.sessions])
maxSessionNameLen = maxSessionNameLen if len('SESSIONNAME') < maxSessionNameLen else len('SESSIONNAME')+1
# maxUsernameLen = max([len(self.sessions[i]['Username'])+1 for i in self.sessions])
maxUsernameLen = max([len(self.sessions[i]['Username']+self.sessions[i]['Domain'])+1 for i in self.sessions])+1
maxUsernameLen = maxUsernameLen if len('Username') < maxUsernameLen else len('Username')+1
maxIdLen = max([len(str(i)) for i in self.sessions])
maxIdLen = maxIdLen if len('ID') < maxIdLen else len('ID')+1
maxStateLen = max([len(self.sessions[i]['state'])+1 for i in self.sessions])
maxStateLen = maxStateLen if len('STATE') < maxStateLen else len('STATE')+1
maxRemoteIp = max([len(self.sessions[i]['RemoteIp'])+1 for i in self.sessions])
maxRemoteIp = maxRemoteIp if len('RemoteAddress') < maxRemoteIp else len('RemoteAddress')+1
maxClientName = max([len(self.sessions[i]['ClientName'])+1 for i in self.sessions])
maxClientName = maxClientName if len('ClientName') < maxClientName else len('ClientName')+1
template = ('{SESSIONNAME: <%d} '
'{USERNAME: <%d} '
'{ID: <%d} '
'{STATE: <%d} '
'{DSTATE: <9} '
'{CONNTIME: <20} '
'{DISCTIME: <20} ') % (maxSessionNameLen, maxUsernameLen, maxIdLen, maxStateLen)
template_verbose = ('{CLIENTNAME: <%d} '
'{REMOTEIP: <%d} '
'{RESOLUTION: <11} '
'{TIMEZONE: <15}') % (maxClientName,maxRemoteIp)
result = []
header = template.format(
SESSIONNAME = 'SESSIONNAME',
USERNAME = 'USERNAME',
ID = 'ID',
STATE = 'STATE',
DSTATE = 'Desktop',
CONNTIME = 'ConnectTime',
DISCTIME = 'DisconnectTime',
)
header2 = template.replace(' <','=<').format(
SESSIONNAME = '',
USERNAME = '',
ID = '',
STATE = '',
DSTATE = '',
CONNTIME = '',
DISCTIME = '',
)
header_verbose = ''
header2_verbose = ''
if options.verbose:
header_verbose = template_verbose.format(
CLIENTNAME = 'ClientName',
REMOTEIP = 'RemoteAddress',
RESOLUTION = 'Resolution',
TIMEZONE = 'ClientTimeZone'
)
header2_verbose = template_verbose.replace(' <','=<').format(
CLIENTNAME = '',
REMOTEIP = '',
RESOLUTION = '',
TIMEZONE = ''
)
result.append(header+header_verbose)
result.append(header2+header2_verbose+'\n')
for i in self.sessions:
connectTime = self.sessions[i]['ConnectTime']
connectTime = connectTime.strftime(r'%Y/%m/%d %H:%M:%S') if connectTime.year > 1601 else 'None'
disconnectTime = self.sessions[i]['DisconnectTime']
disconnectTime = disconnectTime.strftime(r'%Y/%m/%d %H:%M:%S') if disconnectTime.year > 1601 else 'None'
userName = self.sessions[i]['Domain'] + '\\' + self.sessions[i]['Username'] if len(self.sessions[i]['Username']) else ''
row = template.format(
SESSIONNAME = self.sessions[i]['SessionName'],
USERNAME = userName,
ID = i,
STATE = self.sessions[i]['state'],
DSTATE = desktop_states[self.sessions[i]['flags']],
CONNTIME = connectTime,
DISCTIME = disconnectTime,
)
row_verbose = ''
if options.verbose:
row_verbose = template_verbose.format(
CLIENTNAME = self.sessions[i]['ClientName'],
REMOTEIP = self.sessions[i]['RemoteIp'],
RESOLUTION = self.sessions[i]['Resolution'],
TIMEZONE = self.sessions[i]['ClientTimeZone']
)
result.append(row+row_verbose)
for row in result:
print(row)
def do_tasklist(self):
options = self.__options
with TSTS.LegacyAPI(self.__smbConnection, options.target_ip) as legacy:
handle = legacy.hRpcWinStationOpenServer()
r = legacy.hRpcWinStationGetAllProcesses(handle)
if not len(r):
return None
maxImageNameLen = max([len(i['ImageName']) for i in r])
maxSidLen = max([len(i['pSid']) for i in r])
if options.verbose:
self.get_session_list()
self.enumerate_sessions_config()
maxUserNameLen = max([len(self.sessions[i]['Username']+self.sessions[i]['Domain'])+1 for i in self.sessions])+1
if maxUserNameLen < 11:
maxUserNameLen = 11
template = ('{imagename: <%d} '
'{pid: <6} '
'{sessid: <6} '
'{sessionName: <16} '
'{sessstate: <11} '
'{sessionuser: <%d} '
'{sid: <%d} '
'{workingset: <12}') % (maxImageNameLen, maxUserNameLen, maxSidLen)
print(template.format(imagename = 'Image Name',
pid = 'PID',
sessionName = 'SessName',
sessid = 'SessID',
sessionuser = 'SessUser',
sessstate = 'State',
sid = 'SID',
workingset = 'Mem Usage'
)
)
print(template.replace(' <','=<').format(imagename = '',
pid = '',
sessionName = '',
sessid = '',
sessionuser = '',
sessstate = '',
sid = '',
workingset = ''
)+'\n'
)
for procInfo in r:
sessId = procInfo['SessionId']
fullUserName = ''
if len(self.sessions[sessId]['Domain']):
fullUserName += self.sessions[sessId]['Domain'] + '\\'
if len(self.sessions[sessId]['Username']):
fullUserName += self.sessions[sessId]['Username']
row = template.replace('{workingset: <12}','{workingset: >10,} K').format(
imagename = procInfo['ImageName'],
pid = procInfo['UniqueProcessId'],
sessionName = self.sessions[sessId]['SessionName'],
sessid = procInfo['SessionId'],
sessstate = self.sessions[sessId]['state'].replace('Disconnected','Disc'),
sid = procInfo['pSid'],
sessionuser = fullUserName,
workingset = procInfo['WorkingSetSize']//1000
)
print(row)
else:
template = '{: <%d} {: <8} {: <11} {: <%d} {: >12}' % (maxImageNameLen, maxSidLen)
print(template.format('Image Name', 'PID', 'Session#', 'SID', 'Mem Usage'))
print(template.replace(': ',':=').format('','','','','')+'\n')
for procInfo in r:
row = template.format(
procInfo['ImageName'],
procInfo['UniqueProcessId'],
procInfo['SessionId'],
procInfo['pSid'],
'{:,} K'.format(procInfo['WorkingSetSize']//1000),
)
print(row)
def do_taskkill(self):
options = self.__options
if options.pid is None and options.name is None:
LOG.error('One of the following is required: -pid, -name')
return
pidList = []
with TSTS.LegacyAPI(self.__smbConnection, options.target_ip) as legacy:
handle = legacy.hRpcWinStationOpenServer()
if options.pid is None and options.name is not None:
r = legacy.hRpcWinStationGetAllProcesses(handle)
if not len(r):
LOG.error('Could not get process list')
return
pidList = [i['UniqueProcessId'] for i in r if i['ImageName'].lower() == options.name.lower()]
if not len(pidList):
LOG.error('Could not find %r in process list' % options.name)
return
else:
pidList = [options.pid]
for pid in pidList:
print('Terminating PID: %d ...' % pid, end='')
try:
if legacy.hRpcWinStationTerminateProcess(handle, pid)['ErrorCode']:
print('OK')
else:
print('FAIL')
except Exception as e:
LOG.error('Error terminating pid: %d' % pid)
LOG.error(str(e))
def do_tscon(self):
options = self.__options
with TSTS.TermSrvSession(self.__smbConnection, options.target_ip) as TSSession:
try:
session_handle = None
print('Connecting SessionID %d to %d ...' % (options.source, options.dest), end='')
try:
session_handle = TSSession.hRpcOpenSession(options.source)
except Exception as e:
print('FAIL')
if e.error_code == 0x80070002:
LOG.error('Could not find source SessionID: %d' % options.source)
else:
LOG.error(str(e))
return
if TSSession.hRpcConnect(hSession = session_handle,
TargetSessionId = options.dest,
Password = options.password)['ErrorCode'] == 0:
print('OK')
else:
print('FAIL')
except Exception as e:
print('FAIL')
if e.error_code == 0x80070002:
LOG.error('Could not find destination SessionID: %d' % options.dest)
elif e.error_code == 0x8007139f:
LOG.error('Session in the invalid state. Did you mean %d -> %d?' % (options.dest, options.source))
else:
LOG.error(str(e))
def do_tsdiscon(self):
options = self.__options
with TSTS.TermSrvSession(self.__smbConnection, options.target_ip) as TSSession:
try:
print('Disconnecting SessionID: %d ...' % options.session, end='')
session_handle = TSSession.hRpcOpenSession(options.session)
if TSSession.hRpcDisconnect(session_handle)['ErrorCode'] == 0:
print('OK')
else:
print('FAIL')
except Exception as e:
print('FAIL')
if e.error_code == 1:
LOG.error('Maybe it is already disconnected?')
elif e.error_code == 0x80070002:
LOG.error('Could not find SessionID: %d' % options.session)
else:
LOG.error(str(e))
def do_logoff(self):
options = self.__options
with TSTS.TermSrvSession(self.__smbConnection, options.target_ip) as TSSession:
try:
print('Signing-out SessionID: %d ...' % options.session, end='')
session_handle = TSSession.hRpcOpenSession(options.session)
if TSSession.hRpcLogoff(session_handle)['ErrorCode'] == 0:
print('OK')
else:
print('FAIL')
except Exception as e:
if e.error_code == 0x10000000:
print('OK')
return
print('FAIL')
if e.error_code == 0x80070002:
LOG.error('Could not find SessionID: %d' % options.session)
else:
LOG.error(str(e))
def do_shutdown(self):
options = self.__options
with TSTS.LegacyAPI(self.__smbConnection, options.target_ip) as legacy:
handle = legacy.hRpcWinStationOpenServer()
flags = 0
flagsList = []
ShutdownFlags = [options.logoff, options.shutdown, options.reboot, options.poweroff]
for k,v in zip(ShutdownFlags, ['logoff', 'shutdown', 'reboot', 'poweroff']):
if k:
flagsList.append(v)
flagsList = '|'.join(flagsList)
for k,v in zip(ShutdownFlags, [1,2,4,8]):
if k:
flags |= v
try:
print('Sending shutdown (%s) event ...' % (flagsList), end='')
resp = legacy.hRpcWinStationShutdownSystem(handle, 0, flags)
if resp['ErrorCode']:
print('OK')
else:
resp.dump()
print('FAIL')
except Exception as e:
print('FAIL')
LOG.error(str(e))
def do_msg(self):
options = self.__options
with TSTS.TermSrvSession(self.__smbConnection, options.target_ip) as TSSession:
try:
print('Sending message to SessionID: %d ...' % options.session, end='')
session_handle = TSSession.hRpcOpenSession(options.session)
if TSSession.hRpcShowMessageBox(session_handle, options.title, options.message)['ErrorCode'] == 0:
print('OK')
else:
print('FAIL')
except Exception as e:
print('FAIL')
if e.error_code == 0x80070002:
LOG.error('Could not find SessionID: %d' % options.session)
else:
LOG.error(str(e))
if __name__ == '__main__':
# Init the example's logger theme
logger.init()
# Explicitly changing the stdout encoding format
if sys.stdout.encoding is None:
# Output is redirected to a file
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
print(version.BANNER)
parser = argparse.ArgumentParser(add_help=True, description="Terminal Services manipulation tool.")
parser.add_argument('target', action='store', help='[[domain/]username[:password]@]<targetName or address>')
parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')
subparsers = parser.add_subparsers(help='actions', dest='action')
# qwinsta: Display information about Remote Desktop Services sessions.
qwinsta_parser = subparsers.add_parser('qwinsta', help='Display information about Remote Desktop Services sessions.')
qwinsta_parser.add_argument('-v', action='store_true', dest='verbose', help='Turn VERBOSE output ON')
# tasklist: Display a list of currently running processes on the system.
tasklist_parser = subparsers.add_parser('tasklist', help='Display a list of currently running processes on the system.')
tasklist_parser.add_argument('-v', action='store_true', dest='verbose', help='Turn VERBOSE output ON')
# taskkill: Terminate tasks by process id (PID) or image name
taskkill_parser = subparsers.add_parser('taskkill', help='Terminate tasks by process id (PID) or image name.')
taskkill_parser.add_argument('-pid', action='store', metavar="PID", type=int, help='Specifies process id (PID)')
taskkill_parser.add_argument('-name', action='store', help='Specifies process name (ImageName). Internally it will'
'execute tasklist to retrieve PID by ImageName.')
# tscon: Attaches a user session to a remote desktop session
tscon_parser = subparsers.add_parser('tscon', help='Attaches a user session to a remote desktop session.')
tscon_parser.add_argument('-source', action='store', metavar="SessionID", type=int, required=True, help='Source SessionId')
tscon_parser.add_argument('-dest', action='store', metavar="SessionID", type=int, required=True, help='Destination SessionId')
tscon_parser.add_argument('-password', action='store', type=str, required=False, help='Destination Session\'s password')
# tsdiscon: Disconnects a Remote Desktop Services session
tsdiscon_parser = subparsers.add_parser('tsdiscon', help='Disconnects a Remote Desktop Services session.')
tsdiscon_parser.add_argument('-session', action='store', metavar="SessionID", type=int, required=True, help='SessionId to disconnect')
# logoff: Sign out a Remote Desktop Services session
logoff_parser = subparsers.add_parser('logoff', help='Sign out a Remote Desktop Services session.')
logoff_parser.add_argument('-session', action='store', metavar="SessionID", type=int, required=True, help='SessionId to sign out')
# shutdown: Remote shutdown
shutdown_parser = subparsers.add_parser('shutdown', help='Remote shutdown, affects ALL sessions and logged-in users!',
description="Send Remote Shutdown event. Affects ALL sessions and logged-in users!")
shutdown_parser_group = shutdown_parser.add_argument_group('Shutdown Flags [Multiple Choice]')
shutdown_parser_group.add_argument('-logoff', action='store_true', help='Forces sessions to logoff.')
shutdown_parser_group.add_argument('-shutdown', action='store_true', help='Shuts down the system.')
shutdown_parser_group.add_argument('-reboot', action='store_true', help='Reboots after shutdown.')
shutdown_parser_group.add_argument('-poweroff', action='store_true', help='Powers off after shutdown.')
# msg: Send a message to Remote Desktop Services session (MSGBOX)
msg_parser = subparsers.add_parser('msg', help='Send a message to Remote Desktop Services session (MSGBOX).')
msg_parser.add_argument('-session', action='store', metavar="SessionID", type=int, required=True, help='Receiver SessionId')
msg_parser.add_argument('-title', action='store', metavar="'Your Title'", type=str, required=False, help='Title of the MessageBox [Optional]')
msg_parser.add_argument('-message', action='store', metavar="'Your Message'", type=str, required=True, help='Contents of the MessageBox')
# Authentication options
group = parser.add_argument_group('authentication')
group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH')
group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)')
group.add_argument('-k', action="store_true",
help='Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on '
'target parameters. If valid credentials cannot be found, it will use the ones specified '
'in the command line')
group.add_argument('-aesKey', action="store", metavar="hex key",
help='AES key to use for Kerberos Authentication (128 or 256 bits)')
group = parser.add_argument_group('connection')
group.add_argument('-dc-ip', action='store', metavar="ip address",
help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in '
'the target parameter')
group.add_argument('-target-ip', action='store', metavar="ip address",
help='IP Address of the target machine. If omitted it will use whatever was specified as target. '
'This is useful when target is the NetBIOS name and you cannot resolve it')
group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port",
help='Destination port to connect to SMB Server')
if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)
options = parser.parse_args()
if options.action is None:
parser.print_help()
LOG.error('Too few arguments...')
sys.exit(1)
if options.debug is True:
logging.getLogger().setLevel(logging.DEBUG)
# Print the Library's installation path
logging.debug(version.getInstallationPath())
else:
logging.getLogger().setLevel(logging.INFO)
domain, username, password, remoteName = parse_target(options.target)
if options.target_ip is None:
options.target_ip = remoteName
if domain is None:
domain = ''
if options.aesKey is not None:
options.k = True
if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None:
from getpass import getpass
password = getpass("Password:")
tsHandler = TSHandler(username, password, domain, options)
try:
tsHandler.run(remoteName, options.target_ip)
except Exception as e:
traceback.print_exc()
logging.error(str(e))