diff --git a/examples/tstool.py b/examples/tstool.py index e6d121454..981dd7ca0 100755 --- a/examples/tstool.py +++ b/examples/tstool.py @@ -21,6 +21,7 @@ # 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) @@ -33,6 +34,8 @@ 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 @@ -45,6 +48,11 @@ 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 @@ -533,6 +541,81 @@ def do_msg(self): 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 '' in invitation: + end_pos = invitation.rfind('') + 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__': @@ -593,6 +676,12 @@ def do_msg(self): 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') diff --git a/impacket/dcerpc/v5/tsts.py b/impacket/dcerpc/v5/tsts.py index 3369ff385..88665a0f3 100644 --- a/impacket/dcerpc/v5/tsts.py +++ b/impacket/dcerpc/v5/tsts.py @@ -50,6 +50,7 @@ TermSrvEnumeration_UUID = uuidtup_to_bin(('88143fd0-c28d-4b2b-8fef-8d882f6a9390','1.0')) RCMPublic_UUID = uuidtup_to_bin(('bde95fdf-eee0-45de-9e12-e5a61cd0d4fe','1.0')) RcmListener_UUID = uuidtup_to_bin(('497d95a6-2d27-4bf5-9bbd-a6046957133c','1.0')) +SessEnvPublicRpc_UUID = uuidtup_to_bin(('1257b580-ce2f-4109-82d6-a9459d0bf6bc','1.0')) LegacyAPI_UUID = uuidtup_to_bin(('5ca4a760-ebb1-11cf-8611-00a0245420ed','1.0')) AUDIODRIVENAME_LENGTH = 9 @@ -1899,6 +1900,24 @@ class RpcGetRemoteAddressResponse(NDRCALL): ('ErrorCode', ULONG), ) + +# 3.5.4.1.6 RpcShadow2 (Opnum 0) +class RpcShadow2(NDRCALL): + opnum = 0 + structure = ( + ('TargetSessionId', ULONG), + ('eRequestControl', SHADOW_CONTROL_REQUEST), + ('eRequestPermission', SHADOW_PERMISSION_REQUEST), + ('cchInvitation', ULONG), + ) + +class RpcShadow2Response(NDRCALL): + structure = ( + ('pePermission', SHADOW_REQUEST_RESPONSE), + ('pszInvitation', WSTR), + ('ErrorCode', ULONG), + ) + #OLD 3.4.4.1.6 RpcShadow (Opnum 5) # Probably deprecated. Taken from [MS-TSTS] – v20080207 class RpcShadow(NDRCALL): @@ -3663,6 +3682,14 @@ def hRpcWinStationOpenSessionDirectory(dce, hServer, pszServerName): request['pszServerName'] = pszServerName return dce.request(request, checkError=False) +# 3.10.4.1.1 RpcShadow2 (Opnum 0) +def hRpcShadow2(dce, TargetSessionId, eRequestControl, eRequestPermission, cchInvitation = 8192): + request = RpcShadow2() + request['TargetSessionId'] = TargetSessionId + request['eRequestControl'] = eRequestControl + request['eRequestPermission'] = eRequestPermission + request['cchInvitation'] = cchInvitation + return dce.request(request, checkError=False) ################################################################################ # Initialization Classes and Helper classes @@ -3773,6 +3800,17 @@ def __init__(self, smb, target_ip, kerberos): hRpcStartListener = hRpcStartListener hRpcIsListening = hRpcIsListening +class SessEnvPublicRpc(TSTSEndpoint): + def __init__(self, smb, target_ip, kerberos): + super().__init__(smb, target_ip, + stringbinding = r'ncacn_np:{}[\pipe\SessEnvPublicRpc]', + endpoint = SessEnvPublicRpc_UUID, + kerberos = kerberos + ) + + hRpcShadow2 = hRpcShadow2 + + class LegacyAPI(TSTSEndpoint): def __init__(self, smb, target_ip, kerberos): super().__init__(smb, target_ip,