|
21 | 21 | # tslogoff: Signs-out a Remote Desktop Services session |
22 | 22 | # shutdown: Remote shutdown |
23 | 23 | # msg: Send a message to Remote Desktop Services session (MSGBOX) |
| 24 | +# shadow: Shadow a Remote Desktop Services session |
24 | 25 | # |
25 | 26 | # Author: |
26 | 27 | # Alexander Korznikov (@nopernik) |
|
33 | 34 | import codecs |
34 | 35 | import logging |
35 | 36 | import sys |
| 37 | +from xml.etree.ElementTree import tostring |
| 38 | +import xml.etree.ElementTree as ET |
36 | 39 | from struct import unpack |
37 | 40 |
|
38 | 41 | from impacket import version |
|
45 | 48 | from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED |
46 | 49 |
|
47 | 50 | 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 | +) |
48 | 56 | import traceback |
49 | 57 |
|
50 | 58 |
|
@@ -533,6 +541,81 @@ def do_msg(self): |
533 | 541 | LOG.error('Could not find SessionID: %d' % options.session) |
534 | 542 | else: |
535 | 543 | 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) |
536 | 619 |
|
537 | 620 |
|
538 | 621 | if __name__ == '__main__': |
@@ -593,6 +676,12 @@ def do_msg(self): |
593 | 676 | msg_parser.add_argument('-title', action='store', metavar="'Your Title'", type=str, required=False, help='Title of the MessageBox [Optional]') |
594 | 677 | msg_parser.add_argument('-message', action='store', metavar="'Your Message'", type=str, required=True, help='Contents of the MessageBox') |
595 | 678 |
|
| 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 | + |
596 | 685 | # Authentication options |
597 | 686 | group = parser.add_argument_group('authentication') |
598 | 687 |
|
|
0 commit comments