Files
alexisbalbachan 3236169747 Fix TS_ALL_PROCESSES_INFO parsing for RpcWinStationGetAllProcesses (#2155)
* Fixed TS_ALL_PROCESSES_INFO parsing for RpcWinStationGetAllProcesses

* Removed ldap3 dependency, fixed mismatched data type for rawsid, added missing test

* Removed unused imports, renamed imagenamesize -> imagename to match docs. Added missing test
2026-04-30 11:56:58 -03:00

747 lines
37 KiB
Python
Executable File

#!/usr/bin/env python3
# Impacket - Collection of Python classes for working with network protocols.
#
# Copyright Fortra, LLC and its affiliated companies
#
# 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)
# shadow: Shadow a Remote Desktop Services session
#
# Author:
# Alexander Korznikov (@nopernik)
#
# Reference for:
# [MS-TSTS]
#
import argparse
import codecs
import logging
import sys
from xml.etree.ElementTree import tostring
import xml.etree.ElementTree as ET
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, lsat, lsad
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, DCERPCException
from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED
from impacket.dcerpc.v5 import tsts as TSTS
from impacket.dcerpc.v5.tsts import (
SHADOW_CONTROL_REQUEST,
SHADOW_PERMISSION_REQUEST,
SHADOW_REQUEST_RESPONSE
)
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
if options.hashes is not None:
self.__lmhash, self.__nthash = options.hashes.split(':')
def connect(self, remoteName, remoteHost):
self.remoteName = remoteName
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
with TSTS.TermSrvEnumeration(self.__smbConnection, self.__options.target_ip, self.__doKerberos) 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
if len(self.sessions):
with TSTS.RCMPublic(self.__smbConnection, self.__options.target_ip, self.__doKerberos) 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
if len(self.sessions):
with TSTS.TermSrvSession(self.__smbConnection, self.__options.target_ip, self.__doKerberos) 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 lookupSids(self):
# Slightly modified code from lookupsid.py
try:
stringbinding = r'ncacn_np:%s[\pipe\lsarpc]' % self.__options.target_ip
rpctransport = transport.DCERPCTransportFactory(stringbinding)
rpctransport.set_smb_connection(self.__smbConnection)
dce = rpctransport.get_dce_rpc()
if self.__doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
dce.connect()
dce.bind(lsat.MSRPC_UUID_LSAT)
sids = list(self.sids.keys())
if len(sids) > 32:
sids = sids[:32] # TODO in future update
resp = lsad.hLsarOpenPolicy2(dce, MAXIMUM_ALLOWED | lsat.POLICY_LOOKUP_NAMES)
policyHandle = resp['PolicyHandle']
try:
resp = lsat.hLsarLookupSids(dce, policyHandle, sids, lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta)
except DCERPCException as e:
if str(e).find('STATUS_SOME_NOT_MAPPED') >= 0:
resp = e.get_packet()
else:
raise
for sid, item in zip(sids,resp['TranslatedNames']['Names']):
# if item['Use'] != SID_NAME_USE.SidTypeUnknown:
domainIndex = item['DomainIndex']
if domainIndex == -1: # Unknown domain
self.sids[sid] = '{}\\{}'.format('???', item['Name'])
elif domainIndex >= 0:
name = '{}\\{}'.format(resp['ReferencedDomains']['Domains'][item['DomainIndex']]['Name'], item['Name'])
self.sids[sid] = name
dce.disconnect()
except:
logging.debug(traceback.format_exc())
def sidToUser(self, sid):
if sid[:2] == 'S-' and sid in self.sids:
return self.sids[sid]
return sid
def do_tasklist(self):
options = self.__options
with TSTS.LegacyAPI(self.__smbConnection, options.target_ip, self.__doKerberos) as legacy:
handle = legacy.hRpcWinStationOpenServer()
process_entry_list = legacy.hRpcWinStationGetAllProcesses(handle)
if not len(process_entry_list):
return None
self.sids = {}
for process_entry in process_entry_list:
sid = process_entry.getSid()
if sid[:2] == 'S-' and sid not in self.sids:
self.sids[sid] = sid
self.lookupSids()
maxImageNameLen = max([len(process_entry.getProcessInfo()['ImageName'].getValue()) for process_entry in process_entry_list])
maxSidLen = max([len(process_entry.getSid()) for process_entry in process_entry_list])
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 process_entry in process_entry_list:
process_info = process_entry.getProcessInfo()
sessId = process_info['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 = process_info['ImageName'].getValue(),
pid = process_info['UniqueProcessId'],
sessionName = self.sessions[sessId]['SessionName'],
sessid = process_info['SessionId'],
sessstate = self.sessions[sessId]['state'].replace('Disconnected','Disc'),
sid = self.sidToUser(process_entry.getSid()),
sessionuser = fullUserName,
workingset = process_info['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 process_entry in process_entry_list:
process_info = process_entry.getProcessInfo()
row = template.format(
process_info['ImageName'].getValue(),
process_info['UniqueProcessId'],
process_info['SessionId'],
self.sidToUser(process_entry.getSid()),
'{:,} K'.format(process_info['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, self.__doKerberos) 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.getProcessInfo()['UniqueProcessId'] for i in r
if i.getProcessInfo()['ImageName'].getValue().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, self.__doKerberos) 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, self.__doKerberos) 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, self.__doKerberos) 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, self.__doKerberos) 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, self.__doKerberos) 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))
def do_shadow(self):
"""
Request a Remote Connection String to shadow a Remote Desktop Services session.
Author: Ilya Yatsenko (@fulc2um)
"""
control = (SHADOW_CONTROL_REQUEST.enumItems.SHADOW_CONTROL_REQUEST_TAKECONTROL
if self.__options.control
else SHADOW_CONTROL_REQUEST.enumItems.SHADOW_CONTROL_REQUEST_VIEW)
perm = (SHADOW_PERMISSION_REQUEST.enumItems.SHADOW_PERMISSION_REQUEST_REQUESTPERMISSION
if self.__options.prompt
else SHADOW_PERMISSION_REQUEST.enumItems.SHADOW_PERMISSION_REQUEST_SILENT)
LOG.info(f"Calling RpcShadow2 (SessionId={self.__options.session}, Control={self.__options.control}, Permission={self.__options.prompt})")
try:
with TSTS.SessEnvPublicRpc(self.__smbConnection, self.__options.target_ip, self.__doKerberos) as sErpc:
response = sErpc.hRpcShadow2(self.__options.session, control, perm, 8192)
if self.__options.debug:
LOG.debug(f"Response: {response.getData()}")
permission = response['pePermission']
invitation = response['pszInvitation']
except DCERPCException as e:
LOG.error(f"RPC Exception: {e}")
return
if permission is not None:
try:
desc = TSTS.enum2value(SHADOW_REQUEST_RESPONSE, permission)
except (KeyError, AttributeError):
desc = "Unknown"
LOG.info(f"Permission: {permission} ({desc})")
if permission == SHADOW_REQUEST_RESPONSE.enumItems.SHADOW_REQUEST_RESPONSE_ALLOW.value:
LOG.info("RpcShadow2 call succeeded!")
if not invitation:
LOG.error("RpcShadow2 failed: No invitation received")
sys.exit(1)
LOG.info(f"Invitation received ({len(invitation)} characters)")
try:
invitation = invitation.rstrip('\x00\r\n').strip()
invitation = ET.fromstring(invitation)
except ET.ParseError:
if invitation.startswith('<') and not invitation.endswith('>'):
if '</E>' in invitation:
end_pos = invitation.rfind('</E>') + 4
invitation = invitation[:end_pos]
try:
invitation = ET.fromstring(invitation)
except ET.ParseError:
invitation = None
else:
invitation = None
else:
invitation = None
if invitation:
invitation = tostring(invitation, encoding='utf-8', method='xml').decode('utf-8')
LOG.info("Invitation is well-formed XML")
with open(self.__options.file, 'w', encoding='utf-8') as f:
f.write(invitation)
LOG.info(f"Saved to {self.__options.file} file")
else:
LOG.error("Invitation does not appear to be well-formed XML")
else:
LOG.error("RpcShadow2 failed: Permission denied")
sys.exit(1)
if __name__ == '__main__':
# 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')
parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output')
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')
shadow_parser = subparsers.add_parser('shadow', help='Shadow a Remote Desktop Services session.')
shadow_parser.add_argument('-session', action='store', metavar="SessionID", type=int, required=True, help='SessionId to shadow')
shadow_parser.add_argument('-control', action='store_true', help='Request control of the session (default is view only)')
shadow_parser.add_argument('-prompt', action='store_true', help='Request user permission (default is silent)')
shadow_parser.add_argument('-file', type=str, help='Save invitation to file', default='invite.msrcIncident')
# 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()
# Init the example's logger theme
logger.init(options.ts, options.debug)
if options.action is None:
parser.print_help()
LOG.error('Too few arguments...')
sys.exit(1)
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))