#!/usr/pkg/bin/python3.13
# 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))
