Skip to content

Instantly share code, notes, and snippets.

@killvxk
Forked from ThePirateWhoSmellsOfSunflowers/atexec_rpc.py
Created December 9, 2024 05:03
Show Gist options
  • Save killvxk/fe9345daa893528b1fb8ee0cd585a343 to your computer and use it in GitHub Desktop.
Save killvxk/fe9345daa893528b1fb8ee0cd585a343 to your computer and use it in GitHub Desktop.

Revisions

  1. @ThePirateWhoSmellsOfSunflowers ThePirateWhoSmellsOfSunflowers renamed this gist Nov 27, 2024. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. @ThePirateWhoSmellsOfSunflowers ThePirateWhoSmellsOfSunflowers revised this gist Nov 27, 2024. 1 changed file with 17 additions and 32 deletions.
    49 changes: 17 additions & 32 deletions atexecng.py
    Original file line number Diff line number Diff line change
    @@ -36,7 +36,7 @@
    from impacket.dcerpc.v5 import tsch, transport, rpcrt
    from impacket.dcerpc.v5.dtypes import NULL
    from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, \
    RPC_C_AUTHN_LEVEL_PKT_PRIVACY
    RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY
    from impacket.examples.utils import parse_target
    from impacket.krb5.keytab import Keytab
    from six import PY2
    @@ -45,7 +45,7 @@

    class TSCH_EXEC:
    def __init__(self, username='', password='', domain='', hashes=None, aesKey=None, doKerberos=False, kdcHost=None,
    command=None, sessionId=None, silentCommand=False):
    command=None, sessionId=None, silentCommand=False, port=str()):
    self.__username = username
    self.__password = password
    self.__domain = domain
    @@ -57,30 +57,38 @@ def __init__(self, username='', password='', domain='', hashes=None, aesKey=None
    self.__command = command
    self.__silentCommand = silentCommand
    self.sessionId = sessionId
    self.__port = port

    if hashes is not None:
    self.__lmhash, self.__nthash = hashes.split(':')

    def bf_rpc_port(self, stringbinding):
    tsch_uuid = tsch.MSRPC_UUID_TSCHS
    for port in range(49650, 49674):
    l,h = self.__port.split('-', 2)
    for port in range(int(l), int(h)):
    binding_string_tsch = stringbinding.format(port)
    print(binding_string_tsch)
    logging.debug('candidate: '+binding_string_tsch)
    rpctransport = transport.DCERPCTransportFactory(binding_string_tsch)
    dce = rpctransport.get_dce_rpc()
    try:
    dce.connect()
    dce.bind(tsch_uuid)
    break
    logging.debug('endpoint found! '+binding_string_tsch)
    dce.disconnect()
    return port
    except rpcrt.DCERPCException as e:
    pass
    except struct.error as e:
    pass
    return port
    logging.error('TSCH RPC endpoint not found!')
    sys.exit(1)

    def play(self, addr):
    stringbinding = 'ncacn_ip_tcp:'+ addr +'[{}]'
    port = self.bf_rpc_port(stringbinding)
    if not '-' in self.__port:
    port = self.__port
    else:
    port = self.bf_rpc_port(stringbinding)
    rpctransport = transport.DCERPCTransportFactory(stringbinding.format(port))

    if hasattr(rpctransport, 'set_credentials'):
    @@ -233,28 +241,6 @@ def cmd_split(cmdline):
    dce.disconnect()
    return

    smbConnection = rpctransport.get_smb_connection()
    waitOnce = True
    while True:
    try:
    logging.info('Attempting to read ADMIN$\\Temp\\%s' % tmpFileName)
    smbConnection.getFile('ADMIN$', 'Temp\\%s' % tmpFileName, output_callback)
    break
    except Exception as e:
    if str(e).find('SHARING') > 0:
    time.sleep(3)
    elif str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0:
    if waitOnce is True:
    # We're giving it the chance to flush the file before giving up
    time.sleep(3)
    waitOnce = False
    else:
    raise
    else:
    raise
    logging.debug('Deleting file ADMIN$\\Temp\\%s' % tmpFileName)
    smbConnection.deleteFile('ADMIN$', 'Temp\\%s' % tmpFileName)

    dce.disconnect()


    @@ -268,8 +254,6 @@ def cmd_split(cmdline):
    parser.add_argument('command', action='store', nargs='*', default=' ', help='command to execute at the target ')
    parser.add_argument('-session-id', action='store', type=int, help='an existed logon session to use (no output, no cmd.exe)')
    parser.add_argument('-ts', action='store_true', help='adds timestamp to every logging output')
    parser.add_argument('-silentcommand', action='store_true', default = True, help='does not execute cmd.exe to run '
    'given command (no output)')
    parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')
    parser.add_argument('-codec', action='store', help='Sets encoding used (codec) from the target\'s output (default '
    '"%s"). If errors are detected, run chcp.com at the target, '
    @@ -289,6 +273,7 @@ def cmd_split(cmdline):
    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('-keytab', action="store", help='Read keys for SPN from keytab file')
    group.add_argument('-port', action="store", metavar = "Y-X", default='49650-49680', help='Port')

    if len(sys.argv)==1:
    parser.print_help()
    @@ -336,5 +321,5 @@ def cmd_split(cmdline):
    options.k = True

    atsvc_exec = TSCH_EXEC(username, password, domain, options.hashes, options.aesKey, options.k, options.dc_ip,
    ' '.join(options.command), options.session_id, options.silentcommand)
    ' '.join(options.command), options.session_id, True, options.port)
    atsvc_exec.play(address)
  3. @ThePirateWhoSmellsOfSunflowers ThePirateWhoSmellsOfSunflowers revised this gist Nov 27, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion atexecng.py
    Original file line number Diff line number Diff line change
    @@ -268,7 +268,7 @@ def cmd_split(cmdline):
    parser.add_argument('command', action='store', nargs='*', default=' ', help='command to execute at the target ')
    parser.add_argument('-session-id', action='store', type=int, help='an existed logon session to use (no output, no cmd.exe)')
    parser.add_argument('-ts', action='store_true', help='adds timestamp to every logging output')
    parser.add_argument('-silentcommand', action='store_true', default = False, help='does not execute cmd.exe to run '
    parser.add_argument('-silentcommand', action='store_true', default = True, help='does not execute cmd.exe to run '
    'given command (no output)')
    parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')
    parser.add_argument('-codec', action='store', help='Sets encoding used (codec) from the target\'s output (default '
  4. @ThePirateWhoSmellsOfSunflowers ThePirateWhoSmellsOfSunflowers revised this gist Nov 27, 2024. 1 changed file with 8 additions and 5 deletions.
    13 changes: 8 additions & 5 deletions atexecng.py
    Original file line number Diff line number Diff line change
    @@ -33,7 +33,7 @@

    from impacket.examples import logger
    from impacket import version
    from impacket.dcerpc.v5 import tsch, transporti, rcprt
    from impacket.dcerpc.v5 import tsch, transport, rpcrt
    from impacket.dcerpc.v5.dtypes import NULL
    from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, \
    RPC_C_AUTHN_LEVEL_PKT_PRIVACY
    @@ -62,22 +62,26 @@ def __init__(self, username='', password='', domain='', hashes=None, aesKey=None
    self.__lmhash, self.__nthash = hashes.split(':')

    def bf_rpc_port(self, stringbinding):
    tsch_uuid = tsch.MSRPC_UUID_TSCHS
    for port in range(49650, 49674):
    binding_string_tsch = stringbinding.format(port)
    rpctransport = transport.DCERPCTransportFactory(binding_string_nrpc)
    print(binding_string_tsch)
    rpctransport = transport.DCERPCTransportFactory(binding_string_tsch)
    dce = rpctransport.get_dce_rpc()
    try:
    dce.connect()
    dce.bind(tsch_uuid, transfer_syntax=bin_to_uuidtup(syntax))
    dce.bind(tsch_uuid)
    break
    except rpcrt.DCERPCException as e:
    pass
    except struct.error as e:
    pass
    return port

    def play(self, addr):
    stringbinding = 'ncacn_ip_tcp:'+ addr +'[{}]'
    rpctransport = transport.DCERPCTransportFactory(stringbinding)
    port = self.bf_rpc_port(stringbinding)
    rpctransport = transport.DCERPCTransportFactory(stringbinding.format(port))

    if hasattr(rpctransport, 'set_credentials'):
    # This method exists only for selected protocol sequences.
    @@ -183,7 +187,6 @@ def cmd_split(cmdline):
    """ % ((xml_escape(cmd) if self.__silentCommand is False else self.__command.split()[0]),
    (xml_escape(args) if self.__silentCommand is False else " ".join(self.__command.split()[1:])))
    taskCreated = False
    print(xml)
    try:
    logging.info('Creating task \\%s' % tmpName)
    tsch.hSchRpcRegisterTask(dce, '\\%s' % tmpName, xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
  5. @ThePirateWhoSmellsOfSunflowers ThePirateWhoSmellsOfSunflowers created this gist Nov 27, 2024.
    337 changes: 337 additions & 0 deletions atexecng.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,337 @@
    #!/usr/bin/env python
    # 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:
    # ATSVC example for some functions implemented, creates, enums, runs, delete jobs
    # This example executes a command on the target machine through the Task Scheduler
    # service. Returns the output of such command
    #
    # Author:
    # Alberto Solino (@agsolino)
    #
    # Reference for:
    # DCE/RPC for TSCH
    #

    from __future__ import division
    from __future__ import print_function
    import string
    import sys
    import argparse
    import time
    import random
    import logging
    import struct

    from impacket.examples import logger
    from impacket import version
    from impacket.dcerpc.v5 import tsch, transporti, rcprt
    from impacket.dcerpc.v5.dtypes import NULL
    from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, \
    RPC_C_AUTHN_LEVEL_PKT_PRIVACY
    from impacket.examples.utils import parse_target
    from impacket.krb5.keytab import Keytab
    from six import PY2

    CODEC = sys.stdout.encoding

    class TSCH_EXEC:
    def __init__(self, username='', password='', domain='', hashes=None, aesKey=None, doKerberos=False, kdcHost=None,
    command=None, sessionId=None, silentCommand=False):
    self.__username = username
    self.__password = password
    self.__domain = domain
    self.__lmhash = ''
    self.__nthash = ''
    self.__aesKey = aesKey
    self.__doKerberos = doKerberos
    self.__kdcHost = kdcHost
    self.__command = command
    self.__silentCommand = silentCommand
    self.sessionId = sessionId

    if hashes is not None:
    self.__lmhash, self.__nthash = hashes.split(':')

    def bf_rpc_port(self, stringbinding):
    for port in range(49650, 49674):
    binding_string_tsch = stringbinding.format(port)
    rpctransport = transport.DCERPCTransportFactory(binding_string_nrpc)
    dce = rpctransport.get_dce_rpc()
    try:
    dce.connect()
    dce.bind(tsch_uuid, transfer_syntax=bin_to_uuidtup(syntax))
    break
    except rpcrt.DCERPCException as e:
    pass
    except struct.error as e:
    pass

    def play(self, addr):
    stringbinding = 'ncacn_ip_tcp:'+ addr +'[{}]'
    rpctransport = transport.DCERPCTransportFactory(stringbinding)

    if hasattr(rpctransport, 'set_credentials'):
    # This method exists only for selected protocol sequences.
    rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash,
    self.__aesKey)
    rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost)
    try:
    self.doStuff(rpctransport)
    except Exception as e:
    if logging.getLogger().level == logging.DEBUG:
    import traceback
    traceback.print_exc()
    logging.error(e)
    if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >=0:
    logging.info('When STATUS_OBJECT_NAME_NOT_FOUND is received, try running again. It might work')

    def doStuff(self, rpctransport):
    def output_callback(data):
    try:
    print(data.decode(CODEC))
    except UnicodeDecodeError:
    logging.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with '
    'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute atexec.py '
    'again with -codec and the corresponding codec')
    print(data.decode(CODEC, errors='replace'))

    def xml_escape(data):
    replace_table = {
    "&": "&",
    '"': """,
    "'": "'",
    ">": ">",
    "<": "&lt;",
    }
    return ''.join(replace_table.get(c, c) for c in data)

    def cmd_split(cmdline):
    cmdline = cmdline.split(" ", 1)
    cmd = cmdline[0]
    args = cmdline[1] if len(cmdline) > 1 else ''

    return [cmd, args]

    dce = rpctransport.get_dce_rpc()

    dce.set_credentials(*rpctransport.get_credentials())
    if self.__doKerberos is True:
    dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
    dce.connect()
    dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
    dce.bind(tsch.MSRPC_UUID_TSCHS)
    tmpName = ''.join([random.choice(string.ascii_letters) for _ in range(8)])
    tmpFileName = tmpName + '.tmp'

    if self.sessionId is not None:
    cmd, args = cmd_split(self.__command)
    else:
    cmd = "cmd.exe"
    args = "/C %s > %%windir%%\\Temp\\%s 2>&1" % (self.__command, tmpFileName)

    xml = """<?xml version="1.0" encoding="UTF-16"?>
    <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
    <Triggers>
    <CalendarTrigger>
    <StartBoundary>2015-07-15T20:35:13.2757294</StartBoundary>
    <Enabled>true</Enabled>
    <ScheduleByDay>
    <DaysInterval>1</DaysInterval>
    </ScheduleByDay>
    </CalendarTrigger>
    </Triggers>
    <Principals>
    <Principal id="LocalSystem">
    <UserId>S-1-5-18</UserId>
    <RunLevel>HighestAvailable</RunLevel>
    </Principal>
    </Principals>
    <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
    <IdleSettings>
    <StopOnIdleEnd>true</StopOnIdleEnd>
    <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>true</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <WakeToRun>false</WakeToRun>
    <ExecutionTimeLimit>P3D</ExecutionTimeLimit>
    <Priority>7</Priority>
    </Settings>
    <Actions Context="LocalSystem">
    <Exec>
    <Command>%s</Command>
    <Arguments>%s</Arguments>
    </Exec>
    </Actions>
    </Task>
    """ % ((xml_escape(cmd) if self.__silentCommand is False else self.__command.split()[0]),
    (xml_escape(args) if self.__silentCommand is False else " ".join(self.__command.split()[1:])))
    taskCreated = False
    print(xml)
    try:
    logging.info('Creating task \\%s' % tmpName)
    tsch.hSchRpcRegisterTask(dce, '\\%s' % tmpName, xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
    taskCreated = True

    logging.info('Running task \\%s' % tmpName)
    done = False

    if self.sessionId is None:
    tsch.hSchRpcRun(dce, '\\%s' % tmpName)
    else:
    try:
    tsch.hSchRpcRun(dce, '\\%s' % tmpName, flags=tsch.TASK_RUN_USE_SESSION_ID, sessionId=self.sessionId)
    except Exception as e:
    if str(e).find('ERROR_FILE_NOT_FOUND') >= 0 or str(e).find('E_INVALIDARG') >= 0 :
    logging.info('The specified session doesn\'t exist!')
    done = True
    else:
    raise

    while not done:
    logging.debug('Calling SchRpcGetLastRunInfo for \\%s' % tmpName)
    resp = tsch.hSchRpcGetLastRunInfo(dce, '\\%s' % tmpName)
    if resp['pLastRuntime']['wYear'] != 0:
    done = True
    else:
    time.sleep(2)

    logging.info('Deleting task \\%s' % tmpName)
    tsch.hSchRpcDelete(dce, '\\%s' % tmpName)
    taskCreated = False
    except tsch.DCERPCSessionError as e:
    logging.error(e)
    e.get_packet().dump()
    finally:
    if taskCreated is True:
    tsch.hSchRpcDelete(dce, '\\%s' % tmpName)

    if self.sessionId is not None:
    dce.disconnect()
    return

    if self.__silentCommand:
    dce.disconnect()
    return

    smbConnection = rpctransport.get_smb_connection()
    waitOnce = True
    while True:
    try:
    logging.info('Attempting to read ADMIN$\\Temp\\%s' % tmpFileName)
    smbConnection.getFile('ADMIN$', 'Temp\\%s' % tmpFileName, output_callback)
    break
    except Exception as e:
    if str(e).find('SHARING') > 0:
    time.sleep(3)
    elif str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0:
    if waitOnce is True:
    # We're giving it the chance to flush the file before giving up
    time.sleep(3)
    waitOnce = False
    else:
    raise
    else:
    raise
    logging.debug('Deleting file ADMIN$\\Temp\\%s' % tmpFileName)
    smbConnection.deleteFile('ADMIN$', 'Temp\\%s' % tmpFileName)

    dce.disconnect()


    # Process command-line arguments.
    if __name__ == '__main__':
    print(version.BANNER)

    parser = argparse.ArgumentParser()

    parser.add_argument('target', action='store', help='[[domain/]username[:password]@]<targetName or address>')
    parser.add_argument('command', action='store', nargs='*', default=' ', help='command to execute at the target ')
    parser.add_argument('-session-id', action='store', type=int, help='an existed logon session to use (no output, no cmd.exe)')
    parser.add_argument('-ts', action='store_true', help='adds timestamp to every logging output')
    parser.add_argument('-silentcommand', action='store_true', default = False, help='does not execute cmd.exe to run '
    'given command (no output)')
    parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')
    parser.add_argument('-codec', action='store', help='Sets encoding used (codec) from the target\'s output (default '
    '"%s"). If errors are detected, run chcp.com at the target, '
    'map the result with '
    'https://docs.python.org/3/library/codecs.html#standard-encodings and then execute wmiexec.py '
    'again with -codec and the corresponding codec ' % CODEC)

    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.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('-keytab', action="store", help='Read keys for SPN from keytab file')

    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)

    if options.codec is not None:
    CODEC = options.codec
    else:
    if CODEC is None:
    CODEC = 'utf-8'

    logging.warning("This will work ONLY on Windows >= Vista")

    if ''.join(options.command) == ' ':
    logging.error('You need to specify a command to execute!')
    sys.exit(1)

    if options.debug is True:
    logging.getLogger().setLevel(logging.DEBUG)
    # Print the Library's installation path
    logging.debug(version.getInstallationPath())
    else:
    logging.getLogger().setLevel(logging.INFO)

    domain, username, password, address = parse_target(options.target)

    if domain is None:
    domain = ''

    if options.keytab is not None:
    Keytab.loadKeysFromKeytab (options.keytab, username, domain, options)
    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:")

    if options.aesKey is not None:
    options.k = True

    atsvc_exec = TSCH_EXEC(username, password, domain, options.hashes, options.aesKey, options.k, options.dc_ip,
    ' '.join(options.command), options.session_id, options.silentcommand)
    atsvc_exec.play(address)