Skip to content

Commit 7bd0d5a

Browse files
authored
[tstool.py update] Remote Desktop Shadowing feature support (#2064)
* added RpcShadow2 function and prereqs. Modified tstool to include shadow function * Resolved requested change: unnecessarily creating a second instance of TSTS.RpcShadow2Response
1 parent 8925c2c commit 7bd0d5a

File tree

2 files changed

+127
-0
lines changed

2 files changed

+127
-0
lines changed

examples/tstool.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# tslogoff: Signs-out a Remote Desktop Services session
2222
# shutdown: Remote shutdown
2323
# msg: Send a message to Remote Desktop Services session (MSGBOX)
24+
# shadow: Shadow a Remote Desktop Services session
2425
#
2526
# Author:
2627
# Alexander Korznikov (@nopernik)
@@ -33,6 +34,8 @@
3334
import codecs
3435
import logging
3536
import sys
37+
from xml.etree.ElementTree import tostring
38+
import xml.etree.ElementTree as ET
3639
from struct import unpack
3740

3841
from impacket import version
@@ -45,6 +48,11 @@
4548
from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED
4649

4750
from impacket.dcerpc.v5 import tsts as TSTS
51+
from impacket.dcerpc.v5.tsts import (
52+
SHADOW_CONTROL_REQUEST,
53+
SHADOW_PERMISSION_REQUEST,
54+
SHADOW_REQUEST_RESPONSE
55+
)
4856
import traceback
4957

5058

@@ -533,6 +541,81 @@ def do_msg(self):
533541
LOG.error('Could not find SessionID: %d' % options.session)
534542
else:
535543
LOG.error(str(e))
544+
545+
def do_shadow(self):
546+
"""
547+
Request a Remote Connection String to shadow a Remote Desktop Services session.
548+
Author: Ilya Yatsenko (@fulc2um)
549+
"""
550+
control = (SHADOW_CONTROL_REQUEST.enumItems.SHADOW_CONTROL_REQUEST_TAKECONTROL
551+
if self.__options.control
552+
else SHADOW_CONTROL_REQUEST.enumItems.SHADOW_CONTROL_REQUEST_VIEW)
553+
554+
perm = (SHADOW_PERMISSION_REQUEST.enumItems.SHADOW_PERMISSION_REQUEST_REQUESTPERMISSION
555+
if self.__options.prompt
556+
else SHADOW_PERMISSION_REQUEST.enumItems.SHADOW_PERMISSION_REQUEST_SILENT)
557+
558+
LOG.info(f"Calling RpcShadow2 (SessionId={self.__options.session}, Control={self.__options.control}, Permission={self.__options.prompt})")
559+
560+
try:
561+
with TSTS.SessEnvPublicRpc(self.__smbConnection, self.__options.target_ip, self.__doKerberos) as sErpc:
562+
response = sErpc.hRpcShadow2(self.__options.session, control, perm, 8192)
563+
564+
if self.__options.debug:
565+
LOG.debug(f"Response: {response.getData()}")
566+
567+
permission = response['pePermission']
568+
invitation = response['pszInvitation']
569+
570+
except DCERPCException as e:
571+
LOG.error(f"RPC Exception: {e}")
572+
return
573+
574+
if permission is not None:
575+
try:
576+
desc = TSTS.enum2value(SHADOW_REQUEST_RESPONSE, permission)
577+
except (KeyError, AttributeError):
578+
desc = "Unknown"
579+
LOG.info(f"Permission: {permission} ({desc})")
580+
581+
if permission == SHADOW_REQUEST_RESPONSE.enumItems.SHADOW_REQUEST_RESPONSE_ALLOW.value:
582+
LOG.info("RpcShadow2 call succeeded!")
583+
584+
if not invitation:
585+
LOG.error("RpcShadow2 failed: No invitation received")
586+
sys.exit(1)
587+
588+
LOG.info(f"Invitation received ({len(invitation)} characters)")
589+
590+
try:
591+
invitation = invitation.rstrip('\x00\r\n').strip()
592+
593+
invitation = ET.fromstring(invitation)
594+
except ET.ParseError:
595+
if invitation.startswith('<') and not invitation.endswith('>'):
596+
if '</E>' in invitation:
597+
end_pos = invitation.rfind('</E>') + 4
598+
invitation = invitation[:end_pos]
599+
try:
600+
invitation = ET.fromstring(invitation)
601+
except ET.ParseError:
602+
invitation = None
603+
else:
604+
invitation = None
605+
else:
606+
invitation = None
607+
608+
if invitation:
609+
invitation = tostring(invitation, encoding='utf-8', method='xml').decode('utf-8')
610+
LOG.info("Invitation is well-formed XML")
611+
with open(self.__options.file, 'w', encoding='utf-8') as f:
612+
f.write(invitation)
613+
LOG.info(f"Saved to {self.__options.file} file")
614+
else:
615+
LOG.error("Invitation does not appear to be well-formed XML")
616+
else:
617+
LOG.error("RpcShadow2 failed: Permission denied")
618+
sys.exit(1)
536619

537620

538621
if __name__ == '__main__':
@@ -593,6 +676,12 @@ def do_msg(self):
593676
msg_parser.add_argument('-title', action='store', metavar="'Your Title'", type=str, required=False, help='Title of the MessageBox [Optional]')
594677
msg_parser.add_argument('-message', action='store', metavar="'Your Message'", type=str, required=True, help='Contents of the MessageBox')
595678

679+
shadow_parser = subparsers.add_parser('shadow', help='Shadow a Remote Desktop Services session.')
680+
shadow_parser.add_argument('-session', action='store', metavar="SessionID", type=int, required=True, help='SessionId to shadow')
681+
shadow_parser.add_argument('-control', action='store_true', help='Request control of the session (default is view only)')
682+
shadow_parser.add_argument('-prompt', action='store_true', help='Request user permission (default is silent)')
683+
shadow_parser.add_argument('-file', type=str, help='Save invitation to file', default='invite.msrcIncident')
684+
596685
# Authentication options
597686
group = parser.add_argument_group('authentication')
598687

impacket/dcerpc/v5/tsts.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
TermSrvEnumeration_UUID = uuidtup_to_bin(('88143fd0-c28d-4b2b-8fef-8d882f6a9390','1.0'))
5151
RCMPublic_UUID = uuidtup_to_bin(('bde95fdf-eee0-45de-9e12-e5a61cd0d4fe','1.0'))
5252
RcmListener_UUID = uuidtup_to_bin(('497d95a6-2d27-4bf5-9bbd-a6046957133c','1.0'))
53+
SessEnvPublicRpc_UUID = uuidtup_to_bin(('1257b580-ce2f-4109-82d6-a9459d0bf6bc','1.0'))
5354
LegacyAPI_UUID = uuidtup_to_bin(('5ca4a760-ebb1-11cf-8611-00a0245420ed','1.0'))
5455

5556
AUDIODRIVENAME_LENGTH = 9
@@ -1899,6 +1900,24 @@ class RpcGetRemoteAddressResponse(NDRCALL):
18991900
('ErrorCode', ULONG),
19001901
)
19011902

1903+
1904+
# 3.5.4.1.6 RpcShadow2 (Opnum 0)
1905+
class RpcShadow2(NDRCALL):
1906+
opnum = 0
1907+
structure = (
1908+
('TargetSessionId', ULONG),
1909+
('eRequestControl', SHADOW_CONTROL_REQUEST),
1910+
('eRequestPermission', SHADOW_PERMISSION_REQUEST),
1911+
('cchInvitation', ULONG),
1912+
)
1913+
1914+
class RpcShadow2Response(NDRCALL):
1915+
structure = (
1916+
('pePermission', SHADOW_REQUEST_RESPONSE),
1917+
('pszInvitation', WSTR),
1918+
('ErrorCode', ULONG),
1919+
)
1920+
19021921
#OLD 3.4.4.1.6 RpcShadow (Opnum 5)
19031922
# Probably deprecated. Taken from [MS-TSTS] – v20080207
19041923
class RpcShadow(NDRCALL):
@@ -3663,6 +3682,14 @@ def hRpcWinStationOpenSessionDirectory(dce, hServer, pszServerName):
36633682
request['pszServerName'] = pszServerName
36643683
return dce.request(request, checkError=False)
36653684

3685+
# 3.10.4.1.1 RpcShadow2 (Opnum 0)
3686+
def hRpcShadow2(dce, TargetSessionId, eRequestControl, eRequestPermission, cchInvitation = 8192):
3687+
request = RpcShadow2()
3688+
request['TargetSessionId'] = TargetSessionId
3689+
request['eRequestControl'] = eRequestControl
3690+
request['eRequestPermission'] = eRequestPermission
3691+
request['cchInvitation'] = cchInvitation
3692+
return dce.request(request, checkError=False)
36663693

36673694
################################################################################
36683695
# Initialization Classes and Helper classes
@@ -3773,6 +3800,17 @@ def __init__(self, smb, target_ip, kerberos):
37733800
hRpcStartListener = hRpcStartListener
37743801
hRpcIsListening = hRpcIsListening
37753802

3803+
class SessEnvPublicRpc(TSTSEndpoint):
3804+
def __init__(self, smb, target_ip, kerberos):
3805+
super().__init__(smb, target_ip,
3806+
stringbinding = r'ncacn_np:{}[\pipe\SessEnvPublicRpc]',
3807+
endpoint = SessEnvPublicRpc_UUID,
3808+
kerberos = kerberos
3809+
)
3810+
3811+
hRpcShadow2 = hRpcShadow2
3812+
3813+
37763814
class LegacyAPI(TSTSEndpoint):
37773815
def __init__(self, smb, target_ip, kerberos):
37783816
super().__init__(smb, target_ip,

0 commit comments

Comments
 (0)