initial commit of the new Mythic 3.0.0 server

This commit is contained in:
its-a-feature
2023-03-03 22:04:56 -06:00
parent 172992f83a
commit 50086924f2
2406 changed files with 53445 additions and 811142 deletions
+4
View File
@@ -8,6 +8,8 @@ docs/_build/
env/
venv/
ENV/
mythic-cli
.DS_Store
env.bak/
venv.bak/
# pycharm
@@ -22,8 +24,10 @@ postgres-docker/database/
rabbitmq-docker/storage/
C2_profiles/
Payload_Types/
InstalledServices/
Docker_Templates/
documentation-docker/content/
documentation-docker/public/
display_output.txt
nginx-docker/config/conf.d/services.conf
## Ignore Visual Studio temporary files, build results, and
-1
View File
@@ -1 +0,0 @@
FROM itsafeaturemythic/python38_sanic_c2profile:0.0.6
-18
View File
@@ -1,18 +0,0 @@
{
"instances": [
{
"ServerHeaders": {
"Server": "NetDNA-cache/2.2",
"Cache-Control": "max-age=0, no-cache",
"Pragma": "no-cache",
"Connection": "keep-alive",
"Content-Type": "application/javascript; charset=utf-8"
},
"port": 80,
"key_path": "",
"cert_path": "",
"use_ssl": false,
"debug": false
}
]
}
-10
View File
@@ -1,10 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Page Not Found!</title>
</head>
<body>
</body>
</html>
-157
View File
@@ -1,157 +0,0 @@
#!/usr/bin/env python3
"""This is an example implementation of a C2 server that processes HTTP
communications with an Agent, performs routing with the Mythic server,
and adds the required Mythic header to the HTTP response to identify
the C2 profile forwarding the request.
"""
from sanic import Sanic
from sanic.response import html, redirect, text, raw
from sanic.exceptions import NotFound
import sys
import asyncio
import ssl
from pathlib import Path
import requests
import json
import os
config = {}
async def print_flush(message):
"""Print message and flush the stdout buffer.
Python's stdout is buffered, so it collects data written
into a buffer before it is written to the terminal. This
forces the buffer to be written to the terminal instead of
waiting for output to eventually occur.
Args:
message: self-explanatory
"""
print(message)
sys.stdout.flush()
def server_error_handler(request, exception):
"""Error handler for Sanic app. Formats server error to be presented.
Args:
request: object containing the HTTP request information
exception: object containing exception information
"""
if request is None:
print("Invalid HTTP Method - Likely HTTPS trying to talk to HTTP")
sys.stdout.flush()
return html("Error: Failed to process request", status=500, headers={})
return html("Error: Requested URL {} not found".format(request.url), status=404, headers=config[request.app.name]['headers'])
async def agent_message(request, **kwargs):
"""This is the route handler that processes a request from the Agent.
Args:
request: object containing the HTTP request information
**kwargs: any additional arguments
Returns:
HTTP response object
"""
global config
try:
if config[request.app.name]['debug']:
await print_flush("agent_message request from: {} with {} and {}".format(request.url, request.cookies, request.headers))
await print_flush(" and URI: {}".format(request.query_string))
if config[request.app.name]['debug']:
await print_flush("Forwarding along to: {}".format(config['mythic_address']))
if request.method == "POST":
# manipulate the request if needed - change the "Mythic" header to match the name of your C2 profile
#await MythicCallbackRPC().add_event_message(message="got a POST message")
response = requests.post(config['mythic_address'], data=request.body, verify=False, cookies=request.cookies, headers={"Mythic": "http", **request.headers})
else:
# manipulate the request if needed - change the "Mythic" header to match the name of your C2 profile
#await MythicCallbackRPC().add_event_message(message="got a GET message")
#msg = await MythicCallbackRPC().encrypt_bytes(with_uuid=True, data="my message".encode(), uuid="eaf10700-cb30-402d-b101-8e35d67cdb41")
#await MythicCallbackRPC().add_event_message(message=msg.response)
response = requests.get(config['mythic_address'] + "?{}".format(request.query_string), verify=False, data=request.body, cookies=request.cookies, headers={"Mythic": "http", **request.headers})
return raw(response.content, headers=config[request.app.name]['headers'], status=response.status_code)
except Exception as e:
if request is None:
await print_flush("Invalid HTTP Method - Likely HTTPS trying to talk to HTTP")
return server_error_handler(request, e)
if config[request.app.name]['debug']:
await print_flush("error in agent_message: {}".format(str(e)))
return server_error_handler(request, e)
if __name__ == "__main__":
sys.path.append("/Mythic/mythic")
from mythic_c2_container.C2ProfileBase import *
from mythic_c2_container.MythicCallbackRPC import *
config_file = open("config.json", 'rb')
main_config = json.loads(config_file.read().decode('utf-8'))
print("Opening config and starting instances...")
sys.stdout.flush()
# basic mapping of the general endpoints to the real endpoints
try:
config['mythic_address'] = os.environ['MYTHIC_ADDRESS']
except Exception as e:
print("failed to find MYTHIC_ADDRESS environment variable")
sys.stdout.flush()
sys.exit(1)
# now look at the specific instances to start
for inst in main_config['instances']:
config[str(inst['port'])] = {'debug': inst['debug'],
'headers': inst['ServerHeaders']}
if inst['debug']:
print("Debugging statements are enabled. This gives more context, but might be a performance hit")
else:
print("Debugging statements are disabled")
sys.stdout.flush()
# now to create an app instance to handle responses
app = Sanic(str(inst['port']))
app.config['REQUEST_MAX_SIZE'] = 1000000000
app.config['REQUEST_TIMEOUT'] = 600
app.config['RESPONSE_TIMEOUT'] = 600
app.add_route(agent_message, "/<uri:path>", methods=['GET','POST'])
app.add_route(agent_message, "/", methods=['GET','POST'])
app.error_handler.add(Exception, server_error_handler)
keyfile = Path(inst['key_path'])
certfile = Path(inst['cert_path'])
if keyfile.is_file() and certfile.is_file():
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(inst['cert_path'], keyfile=inst['key_path'])
if inst['debug']:
server = app.create_server(host="0.0.0.0", port=inst['port'], ssl=context, debug=False, return_asyncio_server=True, access_log=True)
else:
server = app.create_server(host="0.0.0.0", port=inst['port'], ssl=context, debug=False, return_asyncio_server=True, access_log=False)
if inst['debug']:
print("using SSL for port {}".format(inst['port']))
sys.stdout.flush()
else:
if inst['debug']:
print("not using SSL for port {}".format(inst['port']))
sys.stdout.flush()
if inst['debug']:
server = app.create_server(host="0.0.0.0", port=inst['port'], debug=False, return_asyncio_server=True, access_log=True)
else:
server = app.create_server(host="0.0.0.0", port=inst['port'], debug=False, return_asyncio_server=True, access_log=False)
task = asyncio.ensure_future(server)
try:
loop = asyncio.get_event_loop()
def callback(fut):
try:
fetch_count = fut.result()
except:
print("port already in use")
sys.stdout.flush()
sys.exit()
task.add_done_callback(callback)
loop.run_forever()
except:
sys.exit()
loop.stop()
@@ -1,206 +0,0 @@
"""This file provides basic examples of the C2 RPC functions.
The following functions are implemented to provide an example of implementation:
- test
- opsec: checks C2 profile parameters to verify they meet user-specified OPSEC-safe implementations
- config_check: check and validate supplied parameters when an payload request is generated
- redirect_rules: generate redirect rules for a specific payload when called on-demand by operator
Documentation follows Google Python Style Guide for comments:
https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings
"""
from mythic_c2_container.MythicRPC import *
import json
import netifaces
async def test(request):
"""Performs a test.
Args:
request: dict containing the function name, and parameters passed for the payload build.
{"action": func_name, "message": "the input", "task_id": task id num}
Returns:
A RPCResponse object containing status and response message
"""
response = RPCResponse()
response.status = RPCStatus.Success
response.response = "hello"
resp = await MythicRPC().execute("create_event_message", message="Test message", warning=False)
return response
async def opsec(request):
"""Checks C2 profile parameters to verify they meet user-specified OPSEC-safe implementations.
Args:
request: dict containing the function name, and parameters passed for the payload build.
{ "action": "opsec", "parameters": {"param_name": "param_value", "param_name2": "param_value2", ....} }
Returns:
A dict containing either a success or error status/message. For example:
success: {"status": "success", "message": "<your success message here>" }
error: {"status": "error", "error": "<your error message here>" }
"""
# perform OPSEC checks against the parameters. In this example, the callback port
# is checked against common HTTPS ports when the callback host contains "https"
params = request["parameters"]
if "https" in params["callback_host"] and params["callback_port"] not in ["443", "8443", "7443"]:
return {"status": "error", "error": f"Mismatch - HTTPS specified, but port {params['callback_port']}, is not one of the standard port (443, 8443)\n"}
# if no OPSEC checks, just return the following message
# return {"status": "success", "message": "No OPSEC checks performed\n"}
# otherwise, indicate that OPSEC checks were successful
return {"status": "success", "message": "Basic OPSEC checks passed\n"}
async def config_check(request):
"""Check and validate supplied parameters when an payload request is generated.
Args:
request: dict containing the function name, and parameters passed for the payload build.
{ "action": "config_check", "parameters": {"param_name": "param_value", "param_name2": "param_value2", ....} }
Returns:
A dict containing either a success or error status/message. For example:
success: {"status": "success", "message": "<your success message here>" }
error: {"status": "error", "error": "<your error message here>" }
"""
# Open the C2 profile's config.json and, build a list of ports, and confirm port use.
# This example code uses the default config.json.
try:
with open("../c2_code/config.json") as f:
config = json.load(f)
possible_ports = []
for inst in config["instances"]:
possible_ports.append({"port": inst["port"], "use_ssl": inst["use_ssl"]})
if str(inst["port"]) == str(request["parameters"]["callback_port"]):
if "https" in request["parameters"]["callback_host"] and not inst["use_ssl"]:
message = f"C2 Profile container is configured to NOT use SSL on port {inst['port']}, but the callback host for the agent is using https, {request['parameters']['callback_host']}.\n\n"
message += "This means there should be the following connectivity for success:\n"
message += f"Agent via SSL to {request['parameters']['callback_host']} on port {inst['port']}, then redirection to C2 Profile container WITHOUT SSL on port {inst['port']}"
return {"status": "error", "error": message}
elif "https" not in request["parameters"]["callback_host"] and inst["use_ssl"]:
message = f"C2 Profile container is configured to use SSL on port {inst['port']}, but the callback host for the agent is using http, {request['parameters']['callback_host']}.\n\n"
message += "This means there should be the following connectivity for success:\n"
message += f"Agent via NO SSL to {request['parameters']['callback_host']} on port {inst['port']}, then redirection to C2 Profile container WITH SSL on port {inst['port']}"
return {"status": "error", "error": message}
else:
message = f"C2 Profile container and agent configuration match port, {inst['port']}, and SSL expectations.\n"
return {"status": "success", "message": message}
message = f"Failed to find port, {request['parameters']['callback_port']}, in C2 Profile configuration\n"
message += f"This could indicate the use of a redirector, or a mismatch in expected connectivity.\n\n"
message += f"This means there should be the following connectivity for success:\n"
if "https" in request["parameters"]["callback_host"]:
message += f"Agent via HTTPS on port {request['parameters']['callback_port']} to {request['parameters']['callback_host']} (should be a redirector).\n"
else:
message += f"Agent via HTTP on port {request['parameters']['callback_port']} to {request['parameters']['callback_host']} (should be a redirector).\n"
if len(possible_ports) == 1:
message += f"Redirector then forwards request to C2 Profile container on port, {possible_ports[0]['port']}, {'WITH SSL' if possible_ports[0]['use_ssl'] else 'WITHOUT SSL'}"
else:
message += f"Redirector then forwards request to C2 Profile container on one of the following ports: {json.dumps(possible_ports)}\n"
if "https" in request["parameters"]["callback_host"]:
message += f"\nAlternatively, this might mean that you want to do SSL but are not using SSL within your C2 Profile container.\n"
message += f"To add SSL to your C2 profile:\n"
message += f"\t1. Go to the C2 Profile page\n"
message += f"\t2. Click configure for the http profile\n"
message += f"\t3. Change 'use_ssl' to 'true' and make sure the port is {request['parameters']['callback_port']}\n"
message += f"\t4. Click to stop the profile and then start it again\n"
return {"status": "success", "message": message}
except Exception as e:
return {"status": "error", "error": str(e)}
async def redirect_rules(request):
"""Generate redirect rules for a specific payload when called on-demand by operator.
Operationally, users invoke this function from the Payloads page in the Mythic UI with a
dropdown menu for the payload they're interested in. These rules can include functionality
such as Apache mod_rewrite rules, Nginx configurations, etc. This function simply generates
output that the operator must then copy and implement on a redirector.
Args:
request: dict containing the function name, and the same profile parameters that were
passed to the opsec and config_check functions.
{ "action": "redirect_rules", "parameters": {"param_name": "param_value", "param_name2": "param_value2", ....} }
Returns:
A dict containing either a success or error status/message. For example:
success: {"status": "success", "message": "<your success message here>" }
error: {"status": "error", "error": "<your error message here>" }
"""
# This example generates Apache mod_rewrite rules for Mythic C2 profiles
# to redirect non-C2 traffic to another site.
output = "mod_rewrite rules generated from @AndrewChiles' project https://github.com/threatexpress/mythic2modrewrite:\n"
# Get User-Agent
errors = ""
ua = ''
uris = []
if "headers" in request['parameters']:
for header in request['parameters']["headers"]:
if header["key"] == "User-Agent":
ua = header["value"]
else:
errors += "[!] User-Agent Not Found\n"
# Get all profile URIs
if "get_uri" in request['parameters']:
uris.append("/" + request['parameters']["get_uri"])
else:
errors += "[!] No GET URI found\n"
if "post_uri" in request['parameters']:
uris.append("/" + request['parameters']["post_uri"])
else:
errors += "[!] No POST URI found\n"
# Create UA in modrewrite syntax. No regex needed in UA string matching, but () characters must be escaped
ua_string = ua.replace('(', '\(').replace(')', '\)')
# Create URI string in modrewrite syntax. "*" are needed in regex to support GET and uri-append parameters on the URI
uris_string = ".*|".join(uris) + ".*"
try:
interface = netifaces.gateways()['default'][netifaces.AF_INET][1]
address = netifaces.ifaddresses(interface)[netifaces.AF_INET][0]['addr']
c2_rewrite_template = """RewriteRule ^.*$ "{c2server}%{{REQUEST_URI}}" [P,L]"""
c2_rewrite_output = []
with open("../c2_code/config.json") as f:
config = json.load(f)
for inst in config["instances"]:
c2_rewrite_output.append(c2_rewrite_template.format(
c2server=f"https://{address}:{inst['port']}" if inst["use_ssl"] else f"http://{address}:{inst['port']}"
))
except Exception as e:
errors += "[!] Failed to get C2 Profile container IP address. Replace 'c2server' in HTACCESS rules with correct IP\n"
c2_rewrite_output = ["""RewriteRule ^.*$ "{c2server}%{{REQUEST_URI}}" [P,L]"""]
htaccess_template = '''
########################################
## .htaccess START
RewriteEngine On
## C2 Traffic (HTTP-GET, HTTP-POST, HTTP-STAGER URIs)
## Logic: If a requested URI AND the User-Agent matches, proxy the connection to the Teamserver
## Consider adding other HTTP checks to fine tune the check. (HTTP Cookie, HTTP Referer, HTTP Query String, etc)
## Refer to http://httpd.apache.org/docs/current/mod/mod_rewrite.html
## Only allow GET and POST methods to pass to the C2 server
RewriteCond %{{REQUEST_METHOD}} ^(GET|POST) [NC]
## Profile URIs
RewriteCond %{{REQUEST_URI}} ^({uris})$
## Profile UserAgent
RewriteCond %{{HTTP_USER_AGENT}} "{ua}"
{c2servers}
## Redirect all other traffic here
RewriteRule ^.*$ {redirect}/? [L,R=302]
## .htaccess END
########################################
'''
htaccess = htaccess_template.format(uris=uris_string, ua=ua_string, c2servers="\n".join(c2_rewrite_output), redirect="redirect")
output += "\tReplace 'redirect' with the http(s) address of where non-matching traffic should go, ex: https://redirect.com\n"
output += f"\n{htaccess}"
if errors != "":
return {"status": "error", "error": errors}
else:
return {"status": "success", "message": output}
@@ -1,138 +0,0 @@
"""This file configures the C2 parameters to be used by a payload for communications.
Mythic will utilize the defined class inheriting C2Profile to identify the C2 profile
and parameters that are presented to the operator in the payload creation UI. These
parameters are added to the payload's PayloadType (builder.py) so they can be used
during the build process.
"""
from mythic_c2_container.C2ProfileBase import *
class HTTP(C2Profile):
name = "http"
description = "Uses HTTP(S) connections with a simple query parameter or basic POST messages. For more configuration options use dynamicHTTP."
author = "@its_a_feature_"
is_p2p = False
is_server_routed = False
parameters = [
C2ProfileParameter(
name="callback_port",
description="Callback Port",
default_value="80",
verifier_regex="^[0-9]+$",
required=False,
),
C2ProfileParameter(
name="killdate",
description="Kill Date",
parameter_type=ParameterType.Date,
default_value=365,
required=False,
),
C2ProfileParameter(
name="encrypted_exchange_check",
description="Perform Key Exchange",
choices=["T", "F"],
required=False,
parameter_type=ParameterType.ChooseOne,
),
C2ProfileParameter(
name="callback_jitter",
description="Callback Jitter in percent",
default_value="23",
verifier_regex="^[0-9]+$",
required=False,
),
C2ProfileParameter(
name="headers",
description="HTTP Headers",
required=False,
parameter_type=ParameterType.Dictionary,
default_value=[
{
"name": "User-Agent",
"max": 1,
"default_value": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
"default_show": True,
},
{
"name": "Host",
"max": 1,
"default_value": "",
"default_show": False,
},
{
"name": "*",
"max": -1,
"default_value": "",
"default_show": False
}
]
),
C2ProfileParameter(
name="AESPSK",
description="Crypto type",
default_value="aes256_hmac",
parameter_type=ParameterType.ChooseOne,
choices=["aes256_hmac", "none"],
required=False,
crypto_type=True
),
C2ProfileParameter(
name="callback_host",
description="Callback Host",
default_value="https://domain.com",
verifier_regex="^(http|https):\/\/[a-zA-Z0-9]+",
),
C2ProfileParameter(
name="get_uri",
description="GET request URI (don't include leading /)",
default_value="index",
required=False,
),
C2ProfileParameter(
name="post_uri",
description="POST request URI (don't include leading /)",
default_value="data",
required=False,
),
C2ProfileParameter(
name="query_path_name",
description="Name of the query parameter for GET requests",
default_value="q",
required=False,
verifier_regex="^[^\/]",
),
C2ProfileParameter(
name="proxy_host",
description="Proxy Host",
default_value="",
required=False,
verifier_regex="^$|^(http|https):\/\/[a-zA-Z0-9]+",
),
C2ProfileParameter(
name="proxy_port",
description="Proxy Port",
default_value="",
verifier_regex="^$|^[0-9]+$",
required=False,
),
C2ProfileParameter(
name="proxy_user",
description="Proxy Username",
default_value="",
required=False,
),
C2ProfileParameter(
name="proxy_pass",
description="Proxy Password",
default_value="",
required=False,
),
C2ProfileParameter(
name="callback_interval",
description="Callback Interval in seconds",
default_value="10",
verifier_regex="^[0-9]+$",
required=False,
),
]
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
cd /Mythic/mythic
export PYTHONPATH=/Mythic:/Mythic/mythic
python3.8 mythic_service.py
@@ -1,3 +0,0 @@
#!/usr/bin/env python3
from mythic_c2_container import mythic_service
mythic_service.start_service_and_heartbeat(debug=False)
@@ -1,8 +0,0 @@
{
"username": "mythic_user",
"password": "mythic_password",
"virtual_host": "mythic_vhost",
"host": "127.0.0.1",
"name": "hostname",
"container_files_path": "/Mythic/"
}
-1
View File
@@ -1 +0,0 @@
FROM itsafeaturemythic/python38_payload:0.1.1
@@ -1,34 +0,0 @@
from mythic_payloadtype_container.PayloadBuilder import *
from mythic_payloadtype_container.MythicCommandBase import *
from mythic_payloadtype_container.MythicRPC import *
import sys
import json
#define your payload type class here, it must extend the PayloadType class though
class MyAgent(PayloadType):
name = "my_agent" # name that would show up in the UI
file_extension = "exe" # default file extension to use when creating payloads
author = "@YourHandleHere" # author of the payload type
supported_os = [ # supported OS and architecture combos
SupportedOS.Windows, SupportedOS.Linux # update this list with all the OSes your agent supports
]
wrapper = False # does this payload type act as a wrapper for another payloads inside of it?
# if the payload supports any wrapper payloads, list those here
wrapped_payloads = [] # ex: "service_wrapper"
note = "Any note you want to show up about your payload type in the UI"
supports_dynamic_loading = False # setting this to True allows users to only select a subset of commands when generating a payload
build_parameters = [
# these are all the build parameters that will be presented to the user when creating your payload
# we'll leave this blank for now
]
# the names of the c2 profiles that your agent supports
c2_profiles = ["http"]
translation_container = None
# after your class has been instantiated by the mythic_service in this docker container and all required build parameters have values
# then this function is called to actually build the payload
async def build(self) -> BuildResponse:
# this function gets called to create an instance of your payload
resp = BuildResponse(status=BuildStatus.Success)
resp.payload = b""
return resp
@@ -1,48 +0,0 @@
from mythic_payloadtype_container.MythicCommandBase import *
import json
from mythic_payloadtype_container.MythicRPC import *
class CatArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = [
CommandParameter(
name="path",
type=ParameterType.String,
description="path to file (no quotes required)",
parameter_group_info=[ParameterGroupInfo()]
),
]
async def parse_arguments(self):
if len(self.command_line) == 0:
raise ValueError("Must supply a path to a file")
self.add_arg("path", self.command_line)
async def parse_dictionary(self, dictionary_arguments):
if "path" in dictionary_arguments:
self.add_arg("path", dictionary_arguments["path"])
else:
raise ValueError("Missing 'path' argument")
class CatCommand(CommandBase):
cmd = "cat"
needs_admin = False
help_cmd = "cat /path/to/file"
description = "Read the contents of a file and display it to the user. No need for quotes and relative paths are fine"
version = 1
author = "@its_a_feature_"
argument_class = CatArguments
attackmapping = ["T1005", "T1552.001"]
async def create_tasking(self, task: MythicTask) -> MythicTask:
resp = await MythicRPC().execute("create_artifact", task_id=task.id,
artifact="$.NSString.stringWithContentsOfFileEncodingError",
artifact_type="API Called",
)
return task
async def process_response(self, response: AgentResponse):
pass
@@ -1,51 +0,0 @@
from mythic_payloadtype_container.MythicCommandBase import *
import json
from mythic_payloadtype_container.MythicRPC import *
class DownloadArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = []
async def parse_arguments(self):
if len(self.command_line) > 0:
if self.command_line[0] == "{":
temp_json = json.loads(self.command_line)
if "host" in temp_json:
# this means we have tasking from the file browser rather than the popup UI
# the apfell agent doesn't currently have the ability to do _remote_ listings, so we ignore it
self.command_line = temp_json["path"] + "/" + temp_json["file"]
else:
raise Exception("Unsupported JSON")
else:
raise Exception("Must provide a path to download")
class DownloadCommand(CommandBase):
cmd = "download"
needs_admin = False
help_cmd = "download {path to remote file}"
description = "Download a file from the victim machine to the Mythic server in chunks (no need for quotes in the path)."
version = 1
supported_ui_features = ["file_browser:download"]
author = "@its_a_feature_"
parameters = []
attackmapping = ["T1020", "T1030", "T1041"]
argument_class = DownloadArguments
browser_script = [BrowserScript(script_name="download", author="@its_a_feature_"),
BrowserScript(script_name="download_new", author="@its_a_feature_", for_new_ui=True)]
attributes = CommandAttributes(
suggested_command=True
)
async def create_tasking(self, task: MythicTask) -> MythicTask:
resp = await MythicRPC().execute("create_artifact", task_id=task.id,
artifact="$.NSFileHandle.fileHandleForReadingAtPath, readDataOfLength",
artifact_type="API Called",
)
task.display_params = task.args.command_line
return task
async def process_response(self, response: AgentResponse):
pass
@@ -1,37 +0,0 @@
from mythic_payloadtype_container.MythicCommandBase import *
import json
from mythic_payloadtype_container.MythicRPC import *
class ExitArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = []
async def parse_arguments(self):
pass
class ExitCommand(CommandBase):
cmd = "exit"
needs_admin = False
help_cmd = "exit"
description = "This exits the current apfell agent by leveraging the ObjectiveC bridge's NSApplication terminate function."
version = 1
supported_ui_features = ["callback_table:exit"]
author = "@its_a_feature_"
attackmapping = []
argument_class = ExitArguments
attributes = CommandAttributes(
builtin=True
)
async def create_tasking(self, task: MythicTask) -> MythicTask:
resp = await MythicRPC().execute("create_artifact", task_id=task.id,
artifact="$.NSApplication.sharedApplication.terminate",
artifact_type="API Called",
)
return task
async def process_response(self, response: AgentResponse):
pass
@@ -1,62 +0,0 @@
from mythic_payloadtype_container.MythicCommandBase import *
from mythic_payloadtype_container.MythicRPC import *
class JsimportArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = [
CommandParameter(
name="file",
type=ParameterType.File,
description="Select a JXA file to upload",
parameter_group_info=[ParameterGroupInfo()]
)
]
async def parse_arguments(self):
if len(self.command_line) > 0:
if self.command_line[0] == "{":
self.load_args_from_json_string(self.command_line)
else:
raise ValueError("Missing JSON arguments")
else:
raise ValueError("Missing arguments")
pass
async def parse_dictionary(self, dictionary_arguments):
if "file" in dictionary_arguments:
self.add_arg("file", dictionary_arguments["file"])
else:
raise ValueError("Missing 'file' argument")
class JsimportCommand(CommandBase):
cmd = "jsimport"
needs_admin = False
help_cmd = "jsimport"
description = "import a JXA file into memory"
version = 1
author = "@its_a_feature_"
attackmapping = ["T1020", "T1030", "T1041", "T1620", "T1105"]
argument_class = JsimportArguments
async def create_tasking(self, task: MythicTask) -> MythicTask:
file_resp = await MythicRPC().execute("get_file",
file_id=task.args.get_arg("file"),
task_id=task.id,
get_contents=False)
if file_resp.status == MythicRPCStatus.Success:
original_file_name = file_resp.response[0]["filename"]
else:
raise Exception("Error from Mythic: " + str(file_resp.error))
task.display_params = f"{original_file_name} into memory"
file_resp = await MythicRPC().execute("update_file",
file_id=task.args.get_arg("file"),
delete_after_fetch=False,
comment="Uploaded into memory for jsimport")
return task
async def process_response(self, response: AgentResponse):
pass
@@ -1,43 +0,0 @@
from mythic_payloadtype_container.MythicCommandBase import *
import json
class JsimportCallArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = [
CommandParameter(
name="command",
type=ParameterType.String,
description="The command to execute within a file loaded via jsimport",
parameter_group_info=[ParameterGroupInfo()]
)
]
async def parse_arguments(self):
if len(self.command_line) == 0:
raise ValueError("Must supply a path to a file")
self.add_arg("command", self.command_line)
async def parse_dictionary(self, dictionary_arguments):
if "command" in dictionary_arguments:
self.add_arg("command", dictionary_arguments["command"])
else:
raise ValueError("Missing 'command' argument")
class JsimportCallCommand(CommandBase):
cmd = "jsimport_call"
needs_admin = False
help_cmd = "jsimport_call function_call();"
description = "call a function from within the JS file that was imported with 'jsimport'. This function call is appended to the end of the jsimport code and called via eval."
version = 1
author = "@its_a_feature_"
attackmapping = ["T1059.002"]
argument_class = JsimportCallArguments
async def create_tasking(self, task: MythicTask) -> MythicTask:
return task
async def process_response(self, response: AgentResponse):
pass
@@ -1,51 +0,0 @@
from mythic_payloadtype_container.MythicCommandBase import *
import json
from mythic_payloadtype_container.MythicRPC import *
class ListEntitlementsArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = [
CommandParameter(
name="pid",
type=ParameterType.Number,
default_value=-1,
description="Pid of the process to enumerate (-1 for all processes)",
parameter_group_info=[ParameterGroupInfo(
required=False
)]
),
]
async def parse_arguments(self):
if len(self.command_line) == 0:
raise ValueError("Must supply a path to a file")
self.add_arg("pid", int(self.command_line))
async def parse_dictionary(self, dictionary_arguments):
if "pid" in dictionary_arguments:
self.add_arg("pid", dictionary_arguments["pid"])
class ListEntitlementsCommand(CommandBase):
cmd = "list_entitlements"
needs_admin = False
help_cmd = 'list_entitlements [pid]'
description = "This uses JXA to list the entitlements for a running process"
version = 1
author = "@its_a_feature_"
attackmapping = ["T1057"]
argument_class = ListEntitlementsArguments
supported_ui_features = ["list_entitlements:list"]
browser_script = [BrowserScript(script_name="list_entitlements_new", author="@its_a_feature_", for_new_ui=True)]
async def create_tasking(self, task: MythicTask) -> MythicTask:
if task.args.get_arg("pid") == -1:
task.display_params = "for all running applications"
else:
task.display_params = "for pid " + str(task.args.get_arg("pid"))
return task
async def process_response(self, response: AgentResponse):
pass
@@ -1,90 +0,0 @@
from mythic_payloadtype_container.MythicCommandBase import *
from mythic_payloadtype_container.MythicRPC import *
import base64
import sys
class LoadArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = [
CommandParameter(name="commands",
type=ParameterType.ChooseMultiple,
description="One or more commands to send to the agent",
choices_are_all_commands=True),
]
async def parse_arguments(self):
if len(self.command_line) == 0:
raise ValueError("Must supply a set of commands")
self.add_arg("commands", self.command_line.split(" "))
async def parse_dictionary(self, dictionary_arguments):
if "commands" in dictionary_arguments:
if isinstance(dictionary_arguments["commands"], str):
self.add_arg("commands", dictionary_arguments["commands"].split(" "))
else:
self.add_arg("commands", dictionary_arguments["commands"])
else:
raise ValueError("Missing 'commands' argument")
class LoadCommand(CommandBase):
cmd = "load"
needs_admin = False
help_cmd = "load cmd1 cmd2 cmd3..."
description = "This loads new functions into memory via the C2 channel."
version = 1
author = "@its_a_feature_"
parameters = []
attackmapping = ["T1030", "T1129", "T1059.002", "T1620"]
argument_class = LoadArguments
attributes = CommandAttributes(
suggested_command=True
)
async def create_tasking(self, task: MythicTask) -> MythicTask:
total_code = ""
commands = await MythicRPC().execute("get_commands",
payload_type_name="apfell",
commands=task.args.get_arg("commands"),
os="macOS")
if commands.status == "success":
for cmd in commands.response:
if cmd["script_only"]:
# trying to load a script only command, so just tell mythic to load it
add_resp = await MythicRPC().execute("add_commands_to_callback",
task_id=task.id,
commands=[cmd["cmd"]])
if add_resp.status != "success":
await MythicRPC().execute("create_output", task_id=task.id,
output="Failed to add command to callback: " + add_resp.error)
else:
try:
code_path = self.agent_code_path / "{}.js".format(cmd["cmd"])
total_code += open(code_path, "r").read() + "\n"
except Exception as e:
await MythicRPC().execute("create_output",
task_id=task.id,
output=f"Failed to find code for {cmd['cmd']}, skipping it\n")
if total_code != "":
resp = await MythicRPC().execute("create_file", task_id=task.id,
file=base64.b64encode(total_code.encode()).decode(),
comment="Loading the following commands: " + task.args.command_line
)
if resp.status == MythicStatus.Success:
task.args.add_arg("file_id", resp.response["agent_file_id"])
task.display_params = f"the following commands: {' '.join(task.args.get_arg('commands'))}"
else:
raise Exception("Failed to register file: " + resp.error)
else:
task.status = "completed"
task.display_params = f"the following commands: {' '.join(task.args.get_arg('commands'))}"
await MythicRPC().execute("create_output", task_id=task.id,
output="Loaded commands")
else:
raise Exception("Failed to fetch commands from Mythic: " + commands.error)
return task
async def process_response(self, response: AgentResponse):
pass
@@ -1,73 +0,0 @@
from mythic_payloadtype_container.MythicCommandBase import *
import json
from mythic_payloadtype_container.MythicRPC import *
import sys
class LsArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = [
CommandParameter(
name="path",
type=ParameterType.String,
default_value=".",
description="Path of file or folder on the current system to list",
parameter_group_info=[ParameterGroupInfo(
required=False
)]
),
CommandParameter(
name="fetch_attributes",
type=ParameterType.Boolean,
default_value=False,
description="Indicate if extended attributes should be fetched for this command",
parameter_group_info=[ParameterGroupInfo(
required=False
)]
)
]
async def parse_arguments(self):
self.add_arg("path", self.command_line)
async def parse_dictionary(self, dictionary):
if "host" in dictionary:
# then this came from the file browser
self.add_arg("path", dictionary["path"] + "/" + dictionary["file"])
self.add_arg("file_browser", type=ParameterType.Boolean, value=True)
else:
self.load_args_from_dictionary(dictionary)
class LsCommand(CommandBase):
cmd = "ls"
needs_admin = False
help_cmd = "ls /path/to/file"
description = "Get attributes about a file and display it to the user via API calls. No need for quotes and relative paths are fine"
version = 2
author = "@its_a_feature_"
attackmapping = ["T1106", "T1083"]
supported_ui_features = ["file_browser:list"]
argument_class = LsArguments
browser_script = [BrowserScript(script_name="ls", author="@its_a_feature_"),
BrowserScript(script_name="ls_new", author="@its_a_feature_", for_new_ui=True)]
attributes = CommandAttributes(
spawn_and_injectable=True,
supported_os=[SupportedOS.MacOS],
)
async def create_tasking(self, task: MythicTask) -> MythicTask:
resp = await MythicRPC().execute("create_artifact", task_id=task.id,
artifact="fileManager.attributesOfItemAtPathError, fileManager.contentsOfDirectoryAtPathError",
artifact_type="API Called",
)
if task.args.has_arg("file_browser") and task.args.get_arg("file_browser"):
host = task.callback.host
task.display_params = host + ":" + task.args.get_arg("path")
else:
task.display_params = task.args.get_arg("path")
return task
async def process_response(self, response: AgentResponse):
pass
@@ -1,53 +0,0 @@
from mythic_payloadtype_container.MythicCommandBase import *
import json
from mythic_payloadtype_container.MythicRPC import *
class PlistArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = [
CommandParameter(
name="filename",
type=ParameterType.String,
description="full filename path of type is just read",
parameter_group_info=[ParameterGroupInfo(group_name="read")]
),
CommandParameter(
name="type",
type=ParameterType.ChooseOne,
choices=["readLaunchAgents", "readLaunchDaemons"],
description="read all launchagents/launchdaemons",
default_value="readLaunchAgents",
parameter_group_info=[ParameterGroupInfo()]
),
]
async def parse_arguments(self):
if len(self.command_line) == 0:
raise ValueError("Must supply arguments")
raise ValueError("Must supply named arguments or use the modal")
async def parse_dictionary(self, dictionary_arguments):
self.load_args_from_dictionary(dictionary_arguments)
class PlistCommand(CommandBase):
cmd = "plist"
needs_admin = False
help_cmd = "plist"
description = "Read plists and their associated attributes for attempts to privilege escalate."
version = 1
author = "@its_a_feature_"
attackmapping = ["T1083", "T1007"]
argument_class = PlistArguments
async def create_tasking(self, task: MythicTask) -> MythicTask:
resp = await MythicRPC().execute("create_artifact", task_id=task.id,
artifact="$.NSMutableDictionary.alloc.initWithContentsOfFile, fileManager.attributesOfItemAtPathError",
artifact_type="API Called",
)
return task
async def process_response(self, response: AgentResponse):
pass
@@ -1,88 +0,0 @@
from mythic_payloadtype_container.MythicCommandBase import *
from mythic_payloadtype_container.MythicRPC import *
class ShellArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = [
CommandParameter(name="command", display_name="Command", type=ParameterType.String, description="Command to run"),
]
async def parse_arguments(self):
if len(self.command_line) == 0:
raise ValueError("Must supply a command to run")
self.add_arg("command", self.command_line)
async def parse_dictionary(self, dictionary_arguments):
self.load_args_from_dictionary(dictionary_arguments)
class ShellOPSEC(CommandOPSEC):
injection_method = ""
process_creation = "/bin/bash -c"
authentication = ""
async def opsec_pre(self, task: MythicTask):
# processes = await MythicRPC().execute("search_database", task_id=task.id, table="process",
# host=task.callback.host)
# if processes.status == MythicStatus.Success:
# if len(processes.response) == 0:
# task.opsec_pre_blocked = True
# task.opsec_pre_message = f"This spawns {self.process_creation} and there is no process data on the host yet."
# task.opsec_pre_message += "\nRun \"list_apps\" first to check for dangerous processes"
# task.opsec_pre_bypass_role = "operator"
# return
# else:
# processes = await MythicRPC().execute("search_database", task_id=task.id, table="process",
# name="Microsoft Defender", host=task.callback.host)
# if len(processes.response) > 0:
# task.opsec_pre_blocked = True
# task.opsec_pre_message = f"Microsoft Defender spotted on the host in running processes. Don't spawn commands this way"
# else:
# task.opsec_pre_blocked = True
# task.opsec_pre_message = f"Failed to query processes from Mythic:\n{processes}"
pass
async def opsec_post(self, task: MythicTask):
# processes = await MythicRPC().execute("search_database", task_id=task.id,
# table="process", name="Microsoft Defender", host=task.callback.host)
# if processes.status == MythicStatus.Success:
# if len(processes.response) > 0:
# task.opsec_post_blocked = True
# task.opsec_post_message = f"Microsoft Defender spotted on the host in running processes. Really, don't do this"
# else:
# task.opsec_post_blocked = True
# task.opsec_post_message = f"Failed to query processes from Mythic:\n{processes}"
pass
class ShellCommand(CommandBase):
cmd = "shell"
needs_admin = False
help_cmd = "shell {command}"
description = """This runs {command} in a terminal by leveraging JXA's Application.doShellScript({command}).
WARNING! THIS IS SINGLE THREADED, IF YOUR COMMAND HANGS, THE AGENT HANGS!"""
version = 1
author = "@its_a_feature_"
attackmapping = ["T1059", "T1059.004"]
argument_class = ShellArguments
opsec_class = ShellOPSEC
attributes = CommandAttributes(
suggested_command=True
)
async def create_tasking(self, task: MythicTask) -> MythicTask:
resp = await MythicRPC().execute("create_artifact", task_id=task.id,
artifact="/bin/sh -c {}".format(task.args.get_arg("command")),
artifact_type="Process Create",
)
resp = await MythicRPC().execute("create_artifact", task_id=task.id,
artifact="{}".format(task.args.get_arg("command")),
artifact_type="Process Create",
)
task.display_params = task.args.get_arg("command")
return task
async def process_response(self, response: AgentResponse):
pass
@@ -1,70 +0,0 @@
from mythic_payloadtype_container.MythicCommandBase import *
from mythic_payloadtype_container.MythicRPC import *
import sys
def positiveTime(val):
if val < 0:
raise ValueError("Value must be positive")
class SleepArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = [
CommandParameter(
name="jitter",
type=ParameterType.Number,
validation_func=positiveTime,
description="Percentage of C2's interval to use as jitter",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=2
)]
),
CommandParameter(
name="interval",
type=ParameterType.Number,
validation_func=positiveTime,
description="Number of seconds between checkins",
parameter_group_info=[ParameterGroupInfo(
required=False,
ui_position=1
)]
),
]
async def parse_arguments(self):
if self.command_line[0] != "{":
pieces = self.command_line.split(" ")
if len(pieces) == 1:
self.add_arg("interval", pieces[0])
self.remove_arg("jitter")
elif len(pieces) == 2:
self.add_arg("interval", pieces[0])
self.add_arg("jitter", pieces[1])
else:
raise Exception("Wrong number of parameters, should be 1 or 2")
else:
self.load_args_from_json_string(self.command_line)
class SleepCommand(CommandBase):
cmd = "sleep"
needs_admin = False
help_cmd = "sleep [interval] [jitter]"
description = "Modify the time between callbacks in seconds."
version = 1
author = "@its_a_feature_"
attackmapping = ["T1029"]
argument_class = SleepArguments
async def create_tasking(self, task: MythicTask) -> MythicTask:
task.display_params = str(task.args.get_arg("interval")) + "s"
if task.args.get_arg("jitter") is not None:
task.display_params += " with " + str(task.args.get_arg("jitter")) + "% jitter"
return task
async def process_response(self, response: AgentResponse):
resp = await MythicRPC().execute("update_callback", task_id=response.task.id, sleep_info=response.response)
@@ -1,135 +0,0 @@
from mythic_payloadtype_container.MythicCommandBase import *
from mythic_payloadtype_container.MythicRPC import *
import sys
class UploadArguments(TaskArguments):
def __init__(self, command_line, **kwargs):
super().__init__(command_line, **kwargs)
self.args = [
CommandParameter(
name="file", cli_name="new-file", display_name="File to upload", type=ParameterType.File, description="Select new file to upload",
parameter_group_info=[
ParameterGroupInfo(
required=True,
group_name="Default",
ui_position=0
)
]
),
CommandParameter(
name="filename", cli_name="registered-filename", display_name="Filename within Mythic", description="Supply existing filename in Mythic to upload",
type=ParameterType.ChooseOne,
dynamic_query_function=self.get_files,
parameter_group_info=[
ParameterGroupInfo(
required=True,
ui_position=0,
group_name="specify already uploaded file by name"
)
]
),
CommandParameter(
name="remote_path",
cli_name="remote_path",
display_name="Upload path (with filename)",
type=ParameterType.String,
description="Provide the path where the file will go (include new filename as well)",
parameter_group_info=[
ParameterGroupInfo(
required=True,
group_name="Default",
ui_position=1
),
ParameterGroupInfo(
required=True,
group_name="specify already uploaded file by name",
ui_position=1
)
]
),
]
async def parse_arguments(self):
if len(self.command_line) == 0:
raise ValueError("Must supply arguments")
raise ValueError("Must supply named arguments or use the modal")
async def parse_dictionary(self, dictionary_arguments):
self.load_args_from_dictionary(dictionary_arguments)
async def get_files(self, callback: dict) -> [str]:
file_resp = await MythicRPC().execute("get_file", callback_id=callback["id"],
limit_by_callback=False,
get_contents=False,
filename="",
max_results=-1)
if file_resp.status == MythicRPCStatus.Success:
file_names = []
for f in file_resp.response:
# await MythicRPC().execute("get_file_contents", agent_file_id=f["agent_file_id"])
if f["filename"] not in file_names:
file_names.append(f["filename"])
return file_names
else:
await MythicRPC().execute("create_event_message", warning=True,
message=f"Failed to get files: {file_resp.error}")
return []
class UploadCommand(CommandBase):
cmd = "upload"
needs_admin = False
help_cmd = "upload"
description = (
"Upload a file to the target machine by selecting a file from your computer. "
)
version = 1
supported_ui_features = ["file_browser:upload"]
author = "@its_a_feature_"
attackmapping = ["T1020", "T1030", "T1041", "T1105"]
argument_class = UploadArguments
attributes = CommandAttributes(
suggested_command=True
)
async def create_tasking(self, task: MythicTask) -> MythicTask:
try:
groupName = task.args.get_parameter_group_name()
if groupName == "Default":
file_resp = await MythicRPC().execute("get_file",
file_id=task.args.get_arg("file"),
task_id=task.id,
get_contents=False)
if file_resp.status == MythicRPCStatus.Success:
if len(file_resp.response) > 0:
original_file_name = file_resp.response[0]["filename"]
if len(task.args.get_arg("remote_path")) == 0:
task.args.add_arg("remote_path", original_file_name)
elif task.args.get_arg("remote_path")[-1] == "/":
task.args.add_arg("remote_path", task.args.get_arg("remote_path") + original_file_name)
task.display_params = f"{original_file_name} to {task.args.get_arg('remote_path')}"
else:
raise Exception("Failed to find that file")
else:
raise Exception("Error from Mythic trying to get file: " + str(file_resp.error))
elif groupName == "specify already uploaded file by name":
# we're trying to find an already existing file and use that
file_resp = await MythicRPC().execute("get_file", task_id=task.id,
filename=task.args.get_arg("filename"),
limit_by_callback=False,
get_contents=False)
if file_resp.status == MythicRPCStatus.Success:
if len(file_resp.response) > 0:
task.args.add_arg("file", file_resp.response[0]["agent_file_id"])
task.args.remove_arg("filename")
task.display_params = f"existing {file_resp.response[0]['filename']} to {task.args.get_arg('remote_path')}"
elif len(file_resp.response) == 0:
raise Exception("Failed to find the named file. Have you uploaded it before? Did it get deleted?")
else:
raise Exception("Error from Mythic trying to search files:\n" + str(file_resp.error))
except Exception as e:
raise Exception("Error from Mythic: " + str(sys.exc_info()[-1].tb_lineno) + " : " + str(e))
return task
async def process_response(self, response: AgentResponse):
pass
@@ -1,109 +0,0 @@
function(task, responses){
if(task.status.includes("error")){
const combined = responses.reduce( (prev, cur) => {
return prev + cur;
}, "");
return {'plaintext': combined};
}else if(task.completed){
if(responses.length > 0){
if(responses[0] === "Successfully set the clipboard"){
return {"plaintext": responses[0]};
}else{
try{
let data = JSON.parse(responses[0]);
let output_table = [];
let all_keys = [];
for(const [k,v] of Object.entries(data)){
all_keys.push(k);
if(k === "public.utf8-plain-text"){
output_table.push({
"key":{"plaintext": k},
"value": {"plaintext": atob(v), "copyIcon": v.length > 0},
"fetch": {"button": {
"name": "Fetch Data",
"type": "task",
"ui_feature": "clipboard:list",
"parameters": {"read": [k]}
}},
"view": {"button": {
"name": v=== "" ? "Empty": "View",
"type": "dictionary",
"value": {[k]:atob(v)},
"disabled": v === "",
"leftColumnTitle": "Key",
"rightColumnTitle": "Values",
"title": "Viewing " + k
}}
})
}else{
output_table.push({
"key":{"plaintext": k},
"value": {"plaintext": v, "copyIcon": v.length > 0},
"fetch": {"button": {
"name": "Fetch Data",
"type": "task",
"ui_feature": "clipboard:list",
"parameters":{"read": [k]}
}},
"view": {"button": {
"name": v=== "" ? "Empty": "View",
"type": "dictionary",
"value": {[k]:v},
"disabled": v === "",
"leftColumnTitle": "Key",
"rightColumnTitle": "Values",
"title": "Viewing " + k
}}
})
}
}
output_table.push({
"key":{"plaintext": "Fetch All Clipboard Data"},
"value": {"plaintext": ""},
"fetch": {"button": {
"name": "Fetch All Data",
"type": "task",
"ui_feature": "clipboard:list",
"parameters": {"read": ["*"]}
}},
"view": {"button": {
"name": "View",
"type": "dictionary",
"value": {},
"disabled": true,
"leftColumnTitle": "Key",
"rightColumnTitle": "Values",
"title": "Viewing "
}}
})
return {
"table": [
{
"headers": [
{"plaintext": "fetch", "type": "button", "width": 150, "disableSort": true},
{"plaintext": "view", "type": "button", "width": 100, "disableSort": true},
{"plaintext": "key", "type": "string", "fillWidth": true},
{"plaintext": "value", "type": "string", "fillWidth": true},
],
"rows": output_table,
"title": "Clipboard Data"
}
]
}
}catch(error){
console.log(error);
const combined = responses.reduce( (prev, cur) => {
return prev + cur;
}, "");
return {'plaintext': combined};
}
}
}else{
return {"plaintext": "No output from command"};
}
}else{
return {"plaintext": "No data to display..."};
}
}
@@ -1,33 +0,0 @@
function(task, responses){
console.log(task)
if(task.status.includes("error")){
const combined = responses.reduce( (prev, cur) => {
return prev + cur;
}, "");
return {'plaintext': combined};
}else if(task.completed){
try{
let data = JSON.parse(responses[0]);
return {"download":[{
"agent_file_id": data["agent_file_id"],
"variant": "contained",
"name": "Download file"
}]};
}catch(error){
const combined = responses.reduce( (prev, cur) => {
return prev + cur;
}, "");
return {'plaintext': combined};
}
}else if(task.status === "processed"){
if(responses.length > 0){
const task_data = JSON.parse(responses[0]);
return {"plaintext": "Downloading file with " + task_data["total_chunks"] + " total chunks..."};
}
return {"plaintext": "No data yet..."}
}else{
// this means we shouldn't have any output
return {"plaintext": "No response yet from agent..."}
}
}
@@ -1,72 +0,0 @@
function(task, responses){
if(task.status.includes("error")){
const combined = responses.reduce( (prev, cur) => {
return prev + cur;
}, "");
return {'plaintext': combined};
}else if(task.completed){
if(responses.length > 0){
try{
let data = JSON.parse(responses[0]);
let output_table = [];
for(let i = 0; i < data.length; i++){
output_table.push({
"name":{"plaintext": data[i]["name"]},
"pid": {"plaintext": data[i]["process_id"]},
"bundle": {"plaintext": data[i]["bundle"], "copyIcon": true},
"arch": {"plaintext": data[i]["architecture"]},
"rowStyle": {"backgroundColor": data[i]["frontMost"] ? "mediumpurple": ""},
"actions": {"button": {
"name": "Actions",
"type": "menu",
"value": [
{
"name": "View Paths",
"type": "dictionary",
"value": {
"bundleURL": data[i]["bundleURL"],
"bin_path": data[i]["bin_path"],
},
"leftColumnTitle": "Key",
"rightColumnTitle": "Value",
"title": "Viewing Paths"
},
{
"name": "entitlements",
"type": "task",
"ui_feature": "list_entitlements:list",
"parameters": data[i]["process_id"].toString()
}
]
}},
})
}
return {
"table": [
{
"headers": [
{"plaintext": "pid", "type": "number", "width": 100},
{"plaintext": "name", "type": "string", "fillWidth": true},
{"plaintext": "bundle", "type": "string", "fillWidth": true},
{"plaintext": "arch", "type": "string", "width": 100},
{"plaintext": "actions", "type": "button", "width": 100},
],
"rows": output_table,
"title": "Process Data"
}
]
}
}catch(error){
console.log(error);
const combined = responses.reduce( (prev, cur) => {
return prev + cur;
}, "");
return {'plaintext': combined};
}
}else{
return {"plaintext": "No output from command"};
}
}else{
return {"plaintext": "No data to display..."};
}
}
@@ -1,61 +0,0 @@
function(task, responses){
if(task.status.includes("error")){
const combined = responses.reduce( (prev, cur) => {
return prev + cur;
}, "");
return {'plaintext': combined};
}else if(task.completed){
if(responses.length > 0){
let headers = [
{"plaintext": "pid", "type": "number", "width": 100},
{"plaintext": "name", "type": "string", "fillWidth": true},
{"plaintext": "bundle", "type": "string", "fillWidth": true},
{"plaintext": "entitlements", "type": "button", "width": 200}];
let data = "";
try{
data = JSON.parse(responses[0]);
}catch(error){
const combined = responses.reduce( (prev, cur) => {
return prev + cur;
}, "");
return {'plaintext': combined};
}
let rows = [];
for(let i = 0; i < data.length; i++){
let row = {
"name": {"plaintext": data[i]["name"]},
"pid": {"plaintext": data[i]["pid"]},
"bundle": {"plaintext": data[i]["bundle"], "copyIcon": true},
"entitlements": {"button": {
"name": "",
"startIcon": "list",
"type": "dictionary",
"value": data[i]["entitlements"],
"disabled": Object.keys(data[i]["entitlements"]).length === 0,
"leftColumnTitle": "Entitlements",
"rightColumnTitle": "Value",
"title": "Viewing Entitlements"
}},
};
rows.push(row);
}
return {"table":[{
"headers": headers,
"rows": rows,
"title": "Process Entitlements"
}]};
}else{
const combined = responses.reduce( (prev, cur) => {
return prev + cur;
}, "");
return {'plaintext': combined};
}
}else if(task.status === "processed"){
// this means we're still downloading
return {"plaintext": "Only have partial data so far..."}
}else{
// this means we shouldn't have any output
return {"plaintext": "Not response yet from agent..."}
}
}
@@ -1,143 +0,0 @@
function(task, responses){
if(task.status.includes("error")){
const combined = responses.reduce( (prev, cur) => {
return prev + cur;
}, "");
return {'plaintext': combined};
}else if(task.completed && responses.length > 0){
let data = "";
try{
data = JSON.parse(responses[0]);
}catch(error){
const combined = responses.reduce( (prev, cur) => {
return prev + cur;
}, "");
return {'plaintext': combined};
}
let ls_path = "";
if(data["parent_path"] === "/"){
ls_path = data["parent_path"] + data["name"];
}else{
ls_path = data["parent_path"] + "/" + data["name"];
}
let headers = [
{"plaintext": "actions", "type": "button", "width": 100, "disableSort": true},
{"plaintext": "size", "type": "size", "width": 200},
{"plaintext": "name", "type": "string", "fillWidth": true},
{"plaintext": "owner", "type": "string", "width": 300},
{"plaintext": "group", "type": "string", "width": 300},
{"plaintext": "posix", "type": "string", "width": 100, "disableSort": true},
];
let rows = [{
"rowStyle":{},
"name": {"plaintext": data["name"], "startIcon": data["is_file"] ? "file": "folder"},
"size": {"plaintext": data["size"]},
"owner": {"plaintext": data["permissions"]["owner"]},
"group": {"plaintext": data["permissions"]["group"]},
"posix": {"plaintext": data["permissions"]["posix"]},
"actions": {"button": {
"name": "Actions",
"type": "menu",
"value": [
{
"name": "View XATTRs",
"type": "dictionary",
"value": data["permissions"],
"leftColumnTitle": "XATTR",
"rightColumnTitle": "Values",
"title": "Viewing XATTRs"
},
{
"name": "Get Code Signatures",
"type": "task",
"ui_feature": "code_signatures:list",
"parameters": {"path": ls_path},
},
{
"name": "LS Path",
"type": "task",
"ui_feature": "file_browser:list",
"parameters": ls_path
},
{
"name": "Download File",
"type": "task",
"disabled": !data["is_file"],
"ui_feature": "file_browser:download",
"parameters": ls_path,
"startIcon": "download"
}
]
}}
}];
for(let i = 0; i < data["files"].length; i++){
let ls_path = "";
if(data["parent_path"] === "/"){
ls_path = data["parent_path"] + data["name"] + "/" + data["files"][i]["name"];
}else{
ls_path = data["parent_path"] + "/" + data["name"] + "/" + data["files"][i]["name"];
}
let row = {
"rowStyle": {},
"name": {"plaintext": data["files"][i]["name"], "startIcon": data["files"][i]["is_file"] ? "file": "folder"},
"size": {"plaintext": data["files"][i]["size"]},
"owner": {"plaintext": data["files"][i]["permissions"]["owner"]},
"group": {"plaintext": data["files"][i]["permissions"]["group"]},
"posix": {"plaintext": data["files"][i]["permissions"]["posix"],
"cellStyle": {
}
},
"actions": {"button": {
"name": "Actions",
"type": "menu",
"value": [
{
"name": "View XATTRs",
"type": "dictionary",
"value": data["files"][i]["permissions"],
"leftColumnTitle": "XATTR",
"rightColumnTitle": "Values",
"title": "Viewing XATTRs"
},
{
"name": "Get Code Signatures",
"type": "task",
"ui_feature": "code_signatures:list",
"parameters": ls_path
},
{
"name": "LS Path",
"type": "task",
"ui_feature": "file_browser:list",
"parameters": ls_path
},
{
"name": "Download File",
"type": "task",
"disabled": !data["files"][i]["is_file"],
"ui_feature": "file_browser:download",
"parameters": ls_path,
"startIcon": "download",
"startIconColor": "lightgreen",
"hoverText": "Task agent to download"
}
]
}}
};
rows.push(row);
}
return {"table":[{
"headers": headers,
"rows": rows,
"title": "File Listing Data"
}]};
}else if(task.status === "processed"){
// this means we're still downloading
return {"plaintext": "Only have partial data so far..."}
}else{
// this means we shouldn't have any output
return {"plaintext": "Not response yet from agent..."}
}
}
@@ -1,34 +0,0 @@
function(task, responses){
if(task.status.includes("error")){
const combined = responses.reduce( (prev, cur) => {
return prev + cur;
}, "");
return {'plaintext': combined};
}else if(task.completed){
if(responses.length > 0){
let data = JSON.parse(responses[0]);
return {"screenshot":[{
"agent_file_id": [data["file_id"]],
"variant": "contained",
"name": "View Screenshot"
}]};
}else{
return {"plaintext": "No data to display..."}
}
}else if(task.status === "processed"){
// this means we're still downloading
if(responses.length > 0){
let data = JSON.parse(responses[0]);
return {"screenshot":[{
"agent_file_id": [data["file_id"]],
"variant": "contained",
"name": "View Partial Screenshot"
}]};
}
return {"plaintext": "No data yet..."}
}else{
// this means we shouldn't have any output
return {"plaintext": "Not response yet from agent..."}
}
}
@@ -1,3 +0,0 @@
#!/usr/bin/env python3
from mythic_payloadtype_container import mythic_service
mythic_service.start_service_and_heartbeat()
@@ -1,7 +0,0 @@
#!/bin/bash
cd /Mythic/mythic
export PYTHONPATH=/Mythic:/Mythic/mythic
python3.8 mythic_service.py
@@ -1,8 +0,0 @@
{
"username": "mythic_user",
"password": "mythic_password",
"virtual_host": "mythic_vhost",
"host": "127.0.0.1",
"name": "hostname",
"container_files_path": "/Mythic/"
}
-1
View File
@@ -1 +0,0 @@
FROM itsafeaturemythic/python38_translator_container:0.0.5
View File
@@ -1,81 +0,0 @@
import json
import base64
import sys
# translate_from_c2_format gets a message from Mythic that is in the c2-specific format
# and returns a message that's translated into Mythic's JSON format
# If the associated C2Profile has `mythic_encrypts` set to False, then this function should also decrypt
# the message
# request will be JSON with the following format:
# {
# "enc_key": None or base64 of key if Mythic knows of one,
# "dec_key": None or base64 of key if Mythic knows of one,
# "uuid": uuid of the message,
# "profile": name of the c2 profile,
# "mythic_encrypts": True or False if Mythic thinks Mythic does the encryption or not,
# "type": None or a keyword for the type of encryption. currently only option besides None is "AES256"
# "message": base64 of the message that's currently in c2 specific format
# }
# This should return the JSON of the message in Mythic format
async def translate_from_c2_format(request) -> dict:
if not request["mythic_encrypts"]:
return json.loads(base64.b64decode(request["message"]).decode()[36:])
else:
return json.loads(base64.b64decode(request["message"]))
# translate_to_c2_format gets a message from Mythic that is in Mythic's JSON format
# and returns a message that's formatted into the c2-specific format
# If the associated C2Profile has `mythic_encrypts` set to False, then this function should also encrypt
# the message
# request will be JSON with the following format:
# {
# "enc_key": None or base64 of key if Mythic knows of one,
# "dec_key": None or base64 of key if Mythic knows of one,
# "uuid": uuid of the message,
# "profile": name of the c2 profile,
# "mythic_encrypts": True or False if Mythic thinks Mythic does the encryption or not,
# "type": None or a keyword for the type of encryption. currently only option besides None is "AES256"
# "message": JSON of the mythic message
# }
# This should return the bytes of the message in c2 specific format
async def translate_to_c2_format(request) -> bytes:
if not request["mythic_encrypts"]:
return base64.b64encode(request["uuid"].encode() + json.dumps(request["message"]).encode())
else:
return json.dumps(request["message"]).encode()
# generate_keys gets a message from Mythic that is in Mythic's JSON format
# and returns a a JSON message with encryption and decryption keys for the specified type
# request will be JSON with the following format:
# { "action": "generate_keys",
# "message": JSON of the C2 parameter that has a crypt_type that's not None and not empty
# }
# example:
# {"action":"generate_keys",
# "message":{
# "id":39,
# "name":"AESPSK",
# "default_value":"aes256_hmac\nnone",
# "required":false,
# "randomize":false,
# "verifier_regex":"",
# "parameter_type":"ChooseOne",
# "description":"Crypto type",
# "c2_profile":"http",
# "value":"none",
# "payload":"be8bd7fa-e095-4e69-87aa-a18ba73288cb",
# "instance_name":null,
# "operation":null,
# "callback":null}}
# This should return the dictionary of keys like:
# {
# "enc_key": "base64 of encryption key here",
# "dec_key": "base64 of decryption key here",
# }
async def generate_keys(request) -> dict:
return {"enc_key": None, "dec_key": None}
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
cd /Mythic/mythic
export PYTHONPATH=/Mythic:/Mythic/mythic
python3.8 mythic_service.py
@@ -1,3 +0,0 @@
#!/usr/bin/env python3
from mythic_translator_container import mythic_service
mythic_service.start_service_and_heartbeat()
@@ -1,8 +0,0 @@
{
"username": "mythic_user",
"password": "mythic_password",
"virtual_host": "mythic_vhost",
"host": "127.0.0.1",
"name": "hostname",
"container_files_path": "/Mythic/"
}
+29 -22
View File
@@ -31,28 +31,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## Licenses for other projects used:
## Wait For It - https://github.com/vishnubob/wait-for-it
The MIT License (MIT)
Copyright (c) 2016 Giles Hall
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## arrgv - https://github.com/astur/arrgv
MIT License
@@ -75,3 +53,32 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## https://pkg.go.dev/golang.org/x/mod/semver?tab=licenses
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+12
View File
@@ -0,0 +1,12 @@
.PHONY: default
default: linux ;
linux:
cd Mythic_CLI && make && mv mythic-cli ../
macos:
cd Mythic_CLI && make build_binary_macos && mv mythic-cli ../
build_base_container:
docker image prune -a -f
cd docker-templates && docker build -t mythic-go-python-mono .
+17
View File
@@ -0,0 +1,17 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
.env
mythic-cli
# Dependency directories (remove the comment below to include it)
# vendor/
+17
View File
@@ -0,0 +1,17 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## 0.1.0 - 2023-03-03
## Changed
- Updated to use viper and cobra
## 0.0.8 - 2022-11-7
### Changed
- If the `services` section of the docker-compose.yml file is already set, then the `mythic-cli` binary doesn't modify it. This allows people to make small modifications (such as adding IPv6 addresses) without being overridden. The only field that gets statically changed back each time the mythic-cli binary is run is the `networks.default_network.driver_opts` field since the yaml parser will break up the `com.docker.network.bridge.name` field into subkeys rather than leaving it as a single key.
+11
View File
@@ -0,0 +1,11 @@
FROM itsafeaturemythic/mythic_base_image:0.0.1
WORKDIR /usr/src/app
COPY ["src/", "."]
RUN go get -u github.com/MythicMeta/MythicContainer
RUN go mod download && go mod verify
CMD ["/bin/bash", "-c", "make", "build"]
+9
View File
@@ -0,0 +1,9 @@
// For the code to generate Self-signed certs
Copyright (c) 2012 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+21
View File
@@ -0,0 +1,21 @@
BINARY_NAME=mythic-cli
LOCAL_PATH=$(shell pwd)/src
.PHONY: default
default: build_all_linux ;
build_container:
docker build -t mythic-cli .
build_binary:
docker run --name mythic-cli-builder --rm -v ${LOCAL_PATH}:/usr/src/app mythic-cli make build_linux
mv src/${BINARY_NAME} .
chmod +x ${BINARY_NAME}
build_binary_macos:
docker run --name mythic-cli-builder --rm -v ${LOCAL_PATH}:/usr/src/app mythic-cli make build_macos
mv src/${BINARY_NAME} .
chmod +x ${BINARY_NAME}
build_all_linux: build_container build_binary
build_all_macos: build_container build_binary_macos
+7
View File
@@ -0,0 +1,7 @@
# Mythic_CLI
Golang code for the `mythic-cli` binary in Mythic. This binary provides control for various aspects of Mythic configuration.
## Compilation
Run `make` from the Mythic repo to automatically build the Linux binary and copy it into the Mythic directory.
+15
View File
@@ -0,0 +1,15 @@
BINARY_NAME=mythic-cli
build:
go build -o ${BINARY_NAME} .
build_linux:
GOOS=linux GOARCH=amd64 go build -o ${BINARY_NAME} .
build_macos:
GOOS=darwin GOARCH=amd64 go build -o ${BINARY_NAME} .
run:
./${BINARY_NAME}
build_and_run: build run
+25
View File
@@ -0,0 +1,25 @@
package cmd
import (
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// configCmd represents the config command
var addDockerComposeCmd = &cobra.Command{
Use: "add [service name]",
Short: "Add local service folder to docker compose",
Long: `Run this command to register a local Mythic service folder with docker-compose.`,
Run: addDockerCompose,
Args: cobra.ExactArgs(1),
}
func init() {
rootCmd.AddCommand(addDockerComposeCmd)
}
func addDockerCompose(cmd *cobra.Command, args []string) {
if err := internal.AddDockerComposeEntry(args[0], make(map[string]interface{})); err != nil {
}
}
+25
View File
@@ -0,0 +1,25 @@
package cmd
import (
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// configCmd represents the config command
var buildCmd = &cobra.Command{
Use: "build [container names]",
Short: "Build/rebuild a specific container",
Long: `Run this command to build or rebuild a specific container by specifying container names.`,
Run: buildContainer,
}
func init() {
rootCmd.AddCommand(buildCmd)
}
func buildContainer(cmd *cobra.Command, args []string) {
// initialize tabwriter
if err := internal.DockerBuild(args); err != nil {
}
}
+48
View File
@@ -0,0 +1,48 @@
package cmd
import (
"fmt"
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
"os"
"sort"
"strings"
"text/tabwriter"
)
// configCmd represents the config command
var configCmd = &cobra.Command{
Use: "config",
Short: "Display or adjust the configuration",
Long: `Run this command to display the configuration. Use subcommands to
adjust the configuration or retrieve individual values.`,
Run: configDisplay,
}
func init() {
rootCmd.AddCommand(configCmd)
}
func configDisplay(cmd *cobra.Command, args []string) {
// initialize tabwriter
writer := new(tabwriter.Writer)
// Set minwidth, tabwidth, padding, padchar, and flags
writer.Init(os.Stdout, 8, 8, 1, '\t', 0)
defer writer.Flush()
fmt.Println("[+] Current configuration and available variables:")
fmt.Fprintf(writer, "\n %s\t%s", "Setting", "Value")
fmt.Fprintf(writer, "\n %s\t%s", "–––––––", "–––––––")
configuration := internal.GetConfigAllStrings()
keys := make([]string, 0, len(configuration))
for k := range configuration {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Fprintf(writer, "\n %s\t%s", strings.ToUpper(key), configuration[key])
}
fmt.Fprintln(writer, "")
}
+12
View File
@@ -0,0 +1,12 @@
package config
// Constants and variables used by the Mythic CLI
var (
// Version Mythic CLI version
Version string = "v0.1.0"
BuildDate string
Name string = "Mythic CLI"
DisplayName string = "Mythic CLI"
Description string = "A command line interface for Mythic"
)
+43
View File
@@ -0,0 +1,43 @@
package cmd
import (
"fmt"
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
"os"
"strings"
"text/tabwriter"
)
// configGetCmd represents the configGet command
var configGetCmd = &cobra.Command{
Use: "get <configuration> <configuration> ...",
Short: "Get the specified configuration values",
Long: `Get the specified configuration values. You can provide one value or
a list of values separated by spaces.
For example: mythic-cli config get ADMIN_PASSWORD POSTGRES_PASSWORD`,
Run: configGet,
}
func init() {
configCmd.AddCommand(configGetCmd)
}
func configGet(cmd *cobra.Command, args []string) {
// initialize tabwriter
writer := new(tabwriter.Writer)
// Set minwidth, tabwidth, padding, padchar, and flags
writer.Init(os.Stdout, 8, 8, 1, '\t', 0)
defer writer.Flush()
fmt.Println("[+] Getting configuration values:")
fmt.Fprintf(writer, "\n %s\t%s", "Setting", "Value")
fmt.Fprintf(writer, "\n %s\t%s", "–––––––", "–––––––")
configuration := internal.GetConfigStrings(args)
for key, val := range configuration {
fmt.Fprintf(writer, "\n %s\t%s", strings.ToUpper(key), val)
}
fmt.Fprintln(writer, "")
}
+27
View File
@@ -0,0 +1,27 @@
package cmd
import (
"fmt"
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// configSetCmd represents the configSet command
var configSetCmd = &cobra.Command{
Use: "set <configuration> <value>",
Short: "Set the specified configuration value",
Long: `Set the specified configuration value. Use quotations around the value
if it contains spaces.
For example: mythic-cli config set DATE_FORMAT "d M Y"`,
Args: cobra.ExactArgs(2),
Run: configSet,
}
func init() {
configCmd.AddCommand(configSetCmd)
}
func configSet(cmd *cobra.Command, args []string) {
internal.SetConfigStrings(args[0], args[1])
fmt.Println("[+] Configuration successfully updated. Bring containers down and up for changes to take effect.")
}
+21
View File
@@ -0,0 +1,21 @@
package cmd
import (
"github.com/spf13/cobra"
)
// configCmd represents the config command
var databaseCmd = &cobra.Command{
Use: "database",
Short: "interact with the database",
Long: `Run this command to interact with the database`,
Run: database,
}
func init() {
rootCmd.AddCommand(databaseCmd)
}
func database(cmd *cobra.Command, args []string) {
}
+23
View File
@@ -0,0 +1,23 @@
package cmd
import (
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// configCmd represents the config command
var databaseResetCmd = &cobra.Command{
Use: "reset",
Short: "reset the database",
Long: `Run this command to stop mythic and delete the current database.
THIS DELETES THE CURRENT DATABASE AND ALL DATA WITHIN IT`,
Run: databaseReset,
}
func init() {
databaseCmd.AddCommand(databaseResetCmd)
}
func databaseReset(cmd *cobra.Command, args []string) {
internal.DatabaseReset()
}
+22
View File
@@ -0,0 +1,22 @@
package cmd
import (
"github.com/spf13/cobra"
)
// installCmd represents the config command
var installCmd = &cobra.Command{
Use: "install",
Short: "install services from GitHub or local folders",
Long: `Run this command to install services. Use subcommands to
adjust the URLs / paths of where to pull from.`,
Run: installDisplay,
}
func init() {
rootCmd.AddCommand(installCmd)
}
func installDisplay(cmd *cobra.Command, args []string) {
}
+34
View File
@@ -0,0 +1,34 @@
package cmd
import (
"fmt"
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// installCmd represents the config command
var installFolderCmd = &cobra.Command{
Use: "folder path [-f]",
Short: "install services from local folder",
Long: `Run this command to install a properly formatted ExternalAgent folder into Mythic.`,
Run: installFolder,
Args: cobra.ExactArgs(1),
}
func init() {
installCmd.AddCommand(installFolderCmd)
installFolderCmd.Flags().BoolVar(
&force,
"f",
false,
`Force installing from local folder and don't prompt to overwrite files if an older version is already installed'`,
)
}
func installFolder(cmd *cobra.Command, args []string) {
if err := internal.InstallFolder(args[0], force); err != nil {
fmt.Printf("[-] Failed to install service: %v\n", err)
} else {
fmt.Printf("[+] Successfully installed service!")
}
}
+35
View File
@@ -0,0 +1,35 @@
package cmd
import (
"fmt"
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// installCmd represents the config command
var installGitHubCmd = &cobra.Command{
Use: "github url [branch] [-f]",
Short: "install services from GitHub or other Git-based repositories",
Long: `Run this command to install services from Git-based repositories by doing a git clone`,
Run: installGitHub,
Args: cobra.RangeArgs(1, 2),
}
var force bool
func init() {
installCmd.AddCommand(installGitHubCmd)
installGitHubCmd.Flags().BoolVar(
&force,
"f",
false,
`Force installing from GitHub and don't prompt to overwrite files if an older version is already installed'`,
)
}
func installGitHub(cmd *cobra.Command, args []string) {
if err := internal.InstallService(args[0], args[1], force); err != nil {
fmt.Printf("[-] Failed to install service: %v\n", err)
} else {
fmt.Printf("[+] Successfully installed service!")
}
}
+244
View File
@@ -0,0 +1,244 @@
package internal
import (
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
)
var MythicPossibleServices = []string{
"mythic_postgres",
"mythic_react",
"mythic_server",
"mythic_nginx",
"mythic_rabbitmq",
"mythic_graphql",
"mythic_documentation",
"mythic_jupyter",
"mythic_sync",
"mythic_grafana",
"mythic_prometheus",
"mythic_postgres_exporter",
}
var buildArguments []string
const InstalledServicesFolder = "InstalledServices"
func updateEnvironmentVariables(originalList []string, updates []string) []string {
var finalList []string
for _, entry := range originalList {
entryPieces := strings.Split(entry, "=")
found := false
for _, update := range updates {
updatePieces := strings.Split(update, "=")
if updatePieces[0] == entryPieces[0] {
// the current env vars has a key that we want to update, so don't include the old version
found = true
}
}
if !found {
finalList = append(finalList, entry)
}
}
for _, update := range updates {
finalList = append(finalList, update)
}
return finalList
}
func GetIntendedMythicServiceNames() ([]string, error) {
// need to see about adding services back in if they were for remote hosts before
containerList := []string{}
for _, service := range MythicPossibleServices {
// service is a mythic service, but it's not in our current container list (i.e. not in docker-compose)
switch service {
case "mythic_react":
if mythicEnv.GetString("MYTHIC_REACT_HOST") == "127.0.0.1" || mythicEnv.GetString("MYTHIC_REACT_HOST") == "mythic_react" {
containerList = append(containerList, service)
}
case "mythic_nginx":
if mythicEnv.GetString("NGINX_HOST") == "127.0.0.1" || mythicEnv.GetString("NGINX_HOST") == "mythic_nginx" {
containerList = append(containerList, service)
}
case "mythic_rabbitmq":
if mythicEnv.GetString("RABBITMQ_HOST") == "127.0.0.1" || mythicEnv.GetString("RABBITMQ_HOST") == "mythic_rabbitmq" {
containerList = append(containerList, service)
}
case "mythic_server":
if mythicEnv.GetString("MYTHIC_SERVER_HOST") == "127.0.0.1" || mythicEnv.GetString("MYTHIC_SERVER_HOST") == "mythic_server" {
containerList = append(containerList, service)
}
case "mythic_postgres":
if mythicEnv.GetString("POSTGRES_HOST") == "127.0.0.1" || mythicEnv.GetString("POSTGRES_HOST") == "mythic_postgres" {
containerList = append(containerList, service)
}
case "mythic_graphql":
if mythicEnv.GetString("HASURA_HOST") == "127.0.0.1" || mythicEnv.GetString("HASURA_HOST") == "mythic_graphql" {
containerList = append(containerList, service)
}
case "mythic_documentation":
if mythicEnv.GetString("DOCUMENTATION_HOST") == "127.0.0.1" || mythicEnv.GetString("DOCUMENTATION_HOST") == "mythic_documentation" {
containerList = append(containerList, service)
}
case "mythic_jupyter":
if mythicEnv.GetString("JUPYTER_HOST") == "127.0.0.1" || mythicEnv.GetString("JUPYTER_HOST") == "mythic_jupyter" {
containerList = append(containerList, service)
}
case "mythic_grafana":
if mythicEnv.GetBool("postgres_debug") {
containerList = append(containerList, service)
}
case "mythic_prometheus":
if mythicEnv.GetBool("postgres_debug") {
containerList = append(containerList, service)
}
case "mythic_postgres_exporter":
if mythicEnv.GetBool("postgres_debug") {
containerList = append(containerList, service)
}
}
}
return containerList, nil
}
func getElementsOnDisk() ([]string, error) {
var agentsOnDisk []string
installedServicesFilePath := filepath.Join(getCwdFromExe(), InstalledServicesFolder)
if !dirExists(installedServicesFilePath) {
if err := os.Mkdir(installedServicesFilePath, 0775); err != nil {
return nil, err
}
}
if files, err := ioutil.ReadDir(installedServicesFilePath); err != nil {
log.Printf("[-] Failed to list contents of %s folder\n", InstalledServicesFolder)
return nil, err
} else {
for _, f := range files {
if f.IsDir() {
agentsOnDisk = append(agentsOnDisk, f.Name())
}
}
}
return agentsOnDisk, nil
}
func DockerStart(containers []string) error {
// first stop everything that's currently running
buildArguments = getBuildArguments()
DockerStop(containers)
// make sure that ports are available for us to use
if len(containers) == 0 {
if err := TestPorts(); err != nil {
return err
}
}
// get all the services on disk and in docker-compose currently
if diskAgents, err := getElementsOnDisk(); err != nil {
return err
} else if dockerComposeContainers, err := GetAllExistingNonMythicServiceNames(); err != nil {
return err
} else if intendedMythicServices, err := GetIntendedMythicServiceNames(); err != nil {
return err
} else if currentMythicServices, err := GetCurrentMythicServiceNames(); err != nil {
return err
} else {
for _, val := range currentMythicServices {
if stringInSlice(val, intendedMythicServices) {
} else {
removeMythicServiceDockerComposeEntry(val)
}
}
for _, val := range intendedMythicServices {
if stringInSlice(val, currentMythicServices) {
} else {
addMythicServiceDockerComposeEntry(val)
}
}
// if the user didn't explicitly call out starting certain containers, then do all of them
if len(containers) == 0 {
generateCerts()
containers = append(dockerComposeContainers, intendedMythicServices...)
}
finalContainers := []string{}
//fmt.Printf("container list: %v\n", containers)
//fmt.Printf("dockerComposeContainers: %v\n", dockerComposeContainers)
//fmt.Printf("mythicPossibleServices: %v\n", MythicPossibleServices)
for _, val := range containers { // these are specified containers or all in docker compose
if !stringInSlice(val, dockerComposeContainers) && !stringInSlice(val, MythicPossibleServices) {
if stringInSlice(val, diskAgents) {
// the agent mentioned isn't in docker-compose, but is on disk, ask to add
add := askConfirm(fmt.Sprintf("\n%s isn't in docker-compose, but is on disk. Would you like to add it? ", val))
if add {
finalContainers = append(finalContainers, val)
AddDockerComposeEntry(val, map[string]interface{}{})
}
} else {
add := askConfirm(fmt.Sprintf("\n%s isn't in docker-compose and is not on disk. Would you like to install it from https://github.com/? ", val))
if add {
finalContainers = append(finalContainers, val)
installServiceByName(val)
}
}
} else {
finalContainers = append(finalContainers, val)
}
}
//fmt.Printf("final container list: %v\n", finalContainers)
// update all the mythic service entries to make sure they're the latest
for _, service := range finalContainers {
if stringInSlice(service, MythicPossibleServices) {
addMythicServiceDockerComposeEntry(service)
}
}
if mythicEnv.GetBool("REBUILD_ON_START") {
runDockerCompose(append([]string{"up", "--build", "-d"}, finalContainers...))
} else {
var needToBuild []string
var alreadyBuilt []string
for _, val := range finalContainers {
if !imageExists(val) {
needToBuild = append(needToBuild, val)
} else {
alreadyBuilt = append(alreadyBuilt, val)
}
}
if len(needToBuild) > 0 {
runDockerCompose(append([]string{"up", "--build", "-d"}, needToBuild...))
}
runDockerCompose(append([]string{"up", "-d"}, alreadyBuilt...))
}
TestMythicRabbitmqConnection()
TestMythicConnection()
Status()
return nil
}
}
func DockerStop(containers []string) error {
if dockerComposeContainers, err := GetAllExistingNonMythicServiceNames(); err != nil {
return err
} else if currentMythicServices, err := GetCurrentMythicServiceNames(); err != nil {
return err
} else {
if len(containers) == 0 {
containers = append(dockerComposeContainers, currentMythicServices...)
}
if mythicEnv.GetBool("REBUILD_ON_START") {
runDockerCompose(append([]string{"rm", "-s", "-v", "-f"}, containers...))
} else {
runDockerCompose([]string{"down", "--volumes"})
}
return nil
}
}
func DockerBuild(containers []string) error {
if len(containers) == 0 {
return nil
} else {
runDockerCompose(append([]string{"rm", "-s", "-v", "-f"}, containers...))
runDockerCompose(append([]string{"up", "--build", "-d"}, containers...))
return nil
}
}
@@ -0,0 +1,945 @@
package internal
import (
"bufio"
"context"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/spf13/viper"
"log"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
)
func isServiceRunning(service string) bool {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Fatalf("[-] Failed to get client connection to Docker: %v", err)
}
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{
All: true,
})
if err != nil {
log.Fatalf("[-] Failed to get container list from Docker: %v", err)
}
if len(containers) > 0 {
for _, container := range containers {
if container.Labels["name"] == strings.ToLower(service) {
return true
}
}
}
return false
}
func addMythicServiceDockerComposeEntry(service string) {
var curConfig = viper.New()
curConfig.SetConfigName("docker-compose")
curConfig.SetConfigType("yaml")
curConfig.AddConfigPath(getCwdFromExe())
if err := curConfig.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Fatalf("[-] Error while reading in docker-compose file: %s\n", err)
} else {
log.Fatalf("[-] Error while parsing docker-compose file: %s\n", err)
}
}
networkInfo := map[string]interface{}{
"default_network": map[string]interface{}{
"driver": "bridge",
"driver_opts": map[string]string{
"com.docker.network.bridge.name": "mythic_if",
},
"ipam": map[string]interface{}{
"config": []map[string]interface{}{
{
"subnet": "172.100.0.0/16",
},
},
"driver": "default",
},
"labels": []string{
"mythic_network",
"default_network",
},
},
}
if !curConfig.IsSet("networks") {
// don't blow away changes to the network configuration
curConfig.Set("networks", networkInfo)
} else {
curConfig.Set("networks.default_network.driver_opts", map[string]string{
"com.docker.network.bridge.name": "mythic_if",
})
}
// adding or setting services in the docker-compose file
var pStruct map[string]interface{}
if curConfig.IsSet("services." + strings.ToLower(service)) {
pStruct = curConfig.GetStringMap("services." + strings.ToLower(service))
delete(pStruct, "network_mode")
delete(pStruct, "extra_hosts")
delete(pStruct, "build")
pStruct["networks"] = []string{
"default_network",
}
} else {
pStruct = map[string]interface{}{
"logging": map[string]interface{}{
"driver": "json-file",
"options": map[string]string{
"max-file": "1",
"max-size": "10m",
},
},
"restart": "never",
"labels": map[string]string{
"name": service,
},
"container_name": service,
"image": service,
"networks": []string{
"default_network",
},
}
}
switch service {
case "mythic_postgres":
pStruct["build"] = map[string]interface{}{
"context": "./postgres-docker",
"args": buildArguments,
}
pStruct["healthcheck"] = map[string]interface{}{
"test": "pg_isready -d mythic_db -p ${POSTGRES_PORT} -U mythic_user",
"interval": "30s",
"timeout": "60s",
"retries": 5,
"start_period": "20s",
}
pStruct["command"] = "postgres -c \"max_connections=100\" -p ${POSTGRES_PORT}"
pStruct["volumes"] = []string{
"./postgres-docker/database:/var/lib/postgresql/data",
}
if imageExists("mythic_postgres") {
pStruct["volumes"] = []string{
"./postgres-docker/database:/var/lib/postgresql/data",
"./postgres-docker/postgres.conf:/var/lib/postgresql/postgresql.conf",
"./postgres-docker/postgres.conf:/var/lib/postgresql/data/postgresql.conf",
}
}
pStruct["cpus"] = mythicEnv.GetInt("POSTGRES_CPUS")
if mythicEnv.GetBool("postgres_bind_localhost_only") {
pStruct["ports"] = []string{
"127.0.0.1:${POSTGRES_PORT}:${POSTGRES_PORT}",
}
} else {
pStruct["ports"] = []string{
"${POSTGRES_PORT}:${POSTGRES_PORT}",
}
}
environment := []string{
"POSTGRES_DB=${POSTGRES_DB}",
"POSTGRES_USER=${POSTGRES_USER}",
"POSTGRES_PASSWORD=${POSTGRES_PASSWORD}",
}
if _, ok := pStruct["environment"]; ok {
pStruct["environment"] = updateEnvironmentVariables(curConfig.GetStringSlice("services."+strings.ToLower(service)+".environment"), environment)
} else {
pStruct["environment"] = environment
}
case "mythic_documentation":
pStruct["build"] = "./documentation-docker"
pStruct["build"] = map[string]interface{}{
"context": "./documentation-docker",
"args": buildArguments,
}
pStruct["command"] = "server -p ${DOCUMENTATION_PORT}"
if mythicEnv.GetBool("documentation_bind_localhost_only") {
pStruct["ports"] = []string{
"127.0.0.1:${DOCUMENTATION_PORT}:${DOCUMENTATION_PORT}",
}
} else {
pStruct["ports"] = []string{
"${DOCUMENTATION_PORT}:${DOCUMENTATION_PORT}",
}
}
pStruct["healthcheck"] = map[string]interface{}{
"test": []string{"CMD-SHELL", "wget -nv -t1 -O /dev/null http://127.0.0.1:${DOCUMENTATION_PORT}/docs/"},
"interval": "10s",
"timeout": "10s",
"retries": 5,
"start_period": "10s",
}
pStruct["environment"] = []string{
"DOCUMENTATION_PORT=${DOCUMENTATION_PORT}",
}
pStruct["volumes"] = []string{
"./documentation-docker/:/src",
}
case "mythic_graphql":
pStruct["build"] = map[string]interface{}{
"context": "./hasura-docker",
"args": buildArguments,
}
pStruct["cpus"] = mythicEnv.GetInt("HASURA_CPUS")
environment := []string{
"HASURA_GRAPHQL_DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}",
"HASURA_GRAPHQL_METADATA_DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}",
"HASURA_GRAPHQL_ENABLE_CONSOLE=true",
"HASURA_GRAPHQL_DEV_MODE=false",
"HASURA_GRAPHQL_ADMIN_SECRET=${HASURA_SECRET}",
"HASURA_GRAPHQL_INSECURE_SKIP_TLS_VERIFY=true",
"HASURA_GRAPHQL_SERVER_PORT=${HASURA_PORT}",
"HASURA_GRAPHQL_METADATA_DIR=/metadata",
"HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL=1000",
"HASURA_GRAPHQL_AUTH_HOOK=http://${MYTHIC_SERVER_HOST}:${MYTHIC_SERVER_PORT}/graphql/webhook",
"MYTHIC_ACTIONS_URL_BASE=http://${MYTHIC_SERVER_HOST}:${MYTHIC_SERVER_PORT}/api/v1.4",
"HASURA_GRAPHQL_CONSOLE_ASSETS_DIR=/srv/console-assets",
}
if _, ok := pStruct["environment"]; ok {
pStruct["environment"] = updateEnvironmentVariables(curConfig.GetStringSlice("services."+strings.ToLower(service)+".environment"), environment)
} else {
pStruct["environment"] = environment
}
pStruct["volumes"] = []string{
"./hasura-docker/metadata:/metadata",
}
if mythicEnv.GetBool("hasura_bind_localhost_only") {
pStruct["ports"] = []string{
"127.0.0.1:${HASURA_PORT}:${HASURA_PORT}",
}
} else {
pStruct["ports"] = []string{
"${HASURA_PORT}:${HASURA_PORT}",
}
}
case "mythic_nginx":
pStruct["build"] = map[string]interface{}{
"context": "./nginx-docker",
"args": buildArguments,
}
nginxUseSSL := "ssl"
if !mythicEnv.GetBool("NGINX_USE_SSL") {
nginxUseSSL = ""
}
pStruct["healthcheck"] = map[string]interface{}{
"test": []string{"CMD-SHELL", "curl -k https://127.0.0.1:${NGINX_PORT}"},
"interval": "30s",
"timeout": "60s",
"retries": 5,
"start_period": "15s",
}
environment := []string{
"DOCUMENTATION_HOST=${DOCUMENTATION_HOST}",
"DOCUMENTATION_PORT=${DOCUMENTATION_PORT}",
"NGINX_PORT=${NGINX_PORT}",
"MYTHIC_SERVER_HOST=${MYTHIC_SERVER_HOST}",
"MYTHIC_SERVER_PORT=${MYTHIC_SERVER_PORT}",
"HASURA_HOST=${HASURA_HOST}",
"HASURA_PORT=${HASURA_PORT}",
"MYTHIC_REACT_HOST=${MYTHIC_REACT_HOST}",
"MYTHIC_REACT_PORT=${MYTHIC_REACT_PORT}",
"JUPYTER_HOST=${JUPYTER_HOST}",
"JUPYTER_PORT=${JUPYTER_PORT}",
fmt.Sprintf("NGINX_USE_SSL=%s", nginxUseSSL),
}
if _, ok := pStruct["environment"]; ok {
environment = updateEnvironmentVariables(curConfig.GetStringSlice("services."+strings.ToLower(service)+".environment"), environment)
}
var finalNginxEnv []string
for _, val := range environment {
if !strings.Contains(val, "NEW_UI") {
finalNginxEnv = append(finalNginxEnv, val)
}
}
pStruct["environment"] = finalNginxEnv
pStruct["volumes"] = []string{
"./nginx-docker/ssl:/etc/ssl/private",
"./nginx-docker/config:/etc/nginx",
}
if mythicEnv.GetBool("nginx_bind_localhost_only") {
pStruct["ports"] = []string{
"127.0.0.1:${NGINX_PORT}:${NGINX_PORT}",
}
} else {
pStruct["ports"] = []string{
"${NGINX_PORT}:${NGINX_PORT}",
}
}
case "mythic_rabbitmq":
pStruct["build"] = map[string]interface{}{
"context": "./rabbitmq-docker",
"args": buildArguments,
}
pStruct["healthcheck"] = map[string]interface{}{
"test": []string{"CMD-SHELL", "rabbitmq-diagnostics -q status && rabbitmq-diagnostics -q check_local_alarms"},
"interval": "60s",
"timeout": "30s",
"retries": 5,
"start_period": "15s",
}
pStruct["cpus"] = mythicEnv.GetInt("RABBITMQ_CPUS")
pStruct["command"] = "/bin/sh -c \"chmod +x /generate_config.sh && /generate_config.sh && rabbitmq-server\""
if mythicEnv.GetBool("rabbitmq_bind_localhost_only") {
pStruct["ports"] = []string{
"127.0.0.1:${RABBITMQ_PORT}:${RABBITMQ_PORT}",
}
} else {
pStruct["ports"] = []string{
"${RABBITMQ_PORT}:${RABBITMQ_PORT}",
}
}
environment := []string{
"RABBITMQ_USER=${RABBITMQ_USER}",
"RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD}",
"RABBITMQ_VHOST=${RABBITMQ_VHOST}",
"RABBITMQ_PORT=${RABBITMQ_PORT}",
}
if _, ok := pStruct["environment"]; ok {
environment = updateEnvironmentVariables(curConfig.GetStringSlice("services."+strings.ToLower(service)+".environment"), environment)
}
var finalRabbitEnv []string
badRabbitMqEnvs := []string{
"RABBITMQ_DEFAULT_USER=${RABBITMQ_USER}",
"RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD}",
"RABBITMQ_DEFAULT_VHOST=${RABBITMQ_VHOST}",
}
for _, val := range environment {
if !stringInSlice(val, badRabbitMqEnvs) {
finalRabbitEnv = append(finalRabbitEnv, val)
}
}
pStruct["environment"] = finalRabbitEnv
pStruct["volumes"] = []string{
"./rabbitmq-docker/storage:/var/lib/rabbitmq",
"./rabbitmq-docker/generate_config.sh:/generate_config.sh",
"./rabbitmq-docker/rabbitmq.conf:/tmp/base_rabbitmq.conf",
}
case "mythic_react":
pStruct["build"] = map[string]interface{}{
"context": "./mythic-react-docker",
"args": buildArguments,
}
if mythicEnv.GetBool("mythic_react_bind_localhost_only") {
pStruct["ports"] = []string{
"127.0.0.1:${MYTHIC_REACT_PORT}:${MYTHIC_REACT_PORT}",
}
} else {
pStruct["ports"] = []string{
"${MYTHIC_REACT_PORT}:${MYTHIC_REACT_PORT}",
}
}
pStruct["healthcheck"] = map[string]interface{}{
"test": []string{"CMD-SHELL", "curl -k http://127.0.0.1:${MYTHIC_REACT_PORT}/new"},
"interval": "30s",
"timeout": "60s",
"retries": 3,
"start_period": "15s",
}
pStruct["volumes"] = []string{
"./mythic-react-docker/config:/etc/nginx",
"./mythic-react-docker/mythic/public:/mythic/new",
}
pStruct["environment"] = []string{
"MYTHIC_REACT_PORT=${MYTHIC_REACT_PORT}",
}
case "mythic_jupyter":
pStruct["build"] = map[string]interface{}{
"context": "./jupyter-docker",
"args": buildArguments,
}
pStruct["command"] = "start.sh jupyter lab --ServerApp.open_browser=false --IdentityProvider.token='mythic' --ServerApp.base_url=\"/jupyter\" --ServerApp.default_url=\"/jupyter\""
if mythicEnv.GetBool("jupyter_bind_localhost_only") {
pStruct["ports"] = []string{
"127.0.0.1:${JUPYTER_PORT}:${JUPYTER_PORT}",
}
} else {
pStruct["ports"] = []string{
"${JUPYTER_PORT}:${JUPYTER_PORT}",
}
}
pStruct["volumes"] = []string{
"./jupyter-docker/jupyter:/projects",
}
case "mythic_server":
pStruct["build"] = map[string]interface{}{
"context": "./mythic-docker",
"args": buildArguments,
}
pStruct["volumes"] = []string{
"./mythic-docker/src:/usr/src/app",
}
pStruct["healthcheck"] = map[string]interface{}{
"test": "curl -k http://127.0.0.1:${MYTHIC_SERVER_PORT}/health",
"interval": "60s",
"timeout": "10s",
"retries": 5,
"start_period": "20s",
}
pStruct["command"] = "${MYTHIC_SERVER_COMMAND}"
environment := []string{
"POSTGRES_HOST=${POSTGRES_HOST}",
"POSTGRES_PORT=${POSTGRES_PORT}",
"POSTGRES_PASSWORD=${POSTGRES_PASSWORD}",
"RABBITMQ_HOST=${RABBITMQ_HOST}",
"RABBITMQ_PORT=${RABBITMQ_PORT}",
"RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD}",
"JWT_SECRET=${JWT_SECRET}",
"DEBUG_LEVEL=${DEBUG_LEVEL}",
"MYTHIC_DEBUG_AGENT_MESSAGE=${MYTHIC_DEBUG_AGENT_MESSAGE}",
"MYTHIC_ADMIN_PASSWORD=${MYTHIC_ADMIN_PASSWORD}",
"MYTHIC_ADMIN_USER=${MYTHIC_ADMIN_USER}",
"MYTHIC_SERVER_PORT=${MYTHIC_SERVER_PORT}",
"MYTHIC_SERVER_BIND_LOCALHOST_ONLY=${MYTHIC_SERVER_BIND_LOCALHOST_ONLY}",
"MYTHIC_SERVER_GRPC_PORT=${MYTHIC_SERVER_GRPC_PORT}",
"ALLOWED_IP_BLOCKS=${ALLOWED_IP_BLOCKS}",
"DEFAULT_OPERATION_NAME=${DEFAULT_OPERATION_NAME}",
"NGINX_PORT=${NGINX_PORT}",
"NGINX_HOST=${NGINX_HOST}",
"MYTHIC_SERVER_DYNAMIC_PORTS=${MYTHIC_SERVER_DYNAMIC_PORTS}",
}
mythicServerPorts := []string{
"${MYTHIC_SERVER_PORT}:${MYTHIC_SERVER_PORT}",
"${MYTHIC_SERVER_GRPC_PORT}:${MYTHIC_SERVER_GRPC_PORT}",
}
if mythicEnv.GetBool("MYTHIC_SERVER_BIND_LOCALHOST_ONLY") {
mythicServerPorts = []string{
"127.0.0.1:${MYTHIC_SERVER_PORT}:${MYTHIC_SERVER_PORT}",
"127.0.0.1:${MYTHIC_SERVER_GRPC_PORT}:${MYTHIC_SERVER_GRPC_PORT}",
}
}
dynamicPortPieces := strings.Split(mythicEnv.GetString("MYTHIC_SERVER_DYNAMIC_PORTS"), ",")
for _, val := range dynamicPortPieces {
mythicServerPorts = append(mythicServerPorts, fmt.Sprintf("%s:%s", val, val))
}
pStruct["ports"] = mythicServerPorts
if _, ok := pStruct["environment"]; ok {
pStruct["environment"] = updateEnvironmentVariables(curConfig.GetStringSlice("services."+strings.ToLower(service)+".environment"), environment)
} else {
pStruct["environment"] = environment
}
case "mythic_sync":
if absPath, err := filepath.Abs(filepath.Join(getCwdFromExe(), InstalledServicesFolder, service)); err != nil {
fmt.Printf("[-] Failed to get abs path for mythic_sync")
return
} else {
pStruct["build"] = map[string]interface{}{
"context": absPath,
"args": buildArguments,
}
pStruct["environment"] = []string{
"MYTHIC_IP=${NGINX_HOST}",
"MYTHIC_PORT=${NGINX_PORT}",
"MYTHIC_USERNAME=${MYTHIC_ADMIN_USER}",
"MYTHIC_PASSWORD=${MYTHIC_ADMIN_PASSWORD}",
"MYTHIC_API_KEY=${MYTHIC_API_KEY}",
"GHOSTWRITER_API_KEY=${GHOSTWRITER_API_KEY}",
"GHOSTWRITER_URL=${GHOSTWRITER_URL}",
"GHOSTWRITER_OPLOG_ID=${GHOSTWRITER_OPLOG_ID}",
}
if !mythicEnv.IsSet("GHOSTWRITER_API_KEY") {
key := askVariable("Please enter your GhostWriter API Key")
mythicEnv.Set("GHOSTWRITER_API_KEY", key)
}
if !mythicEnv.IsSet("GHOSTWRITER_URL") {
url := askVariable("Please enter your GhostWriter URL")
mythicEnv.Set("GHOSTWRITER_URL", url)
}
if !mythicEnv.IsSet("GHOSTWRITER_OPLOG_ID") {
gwID := askVariable("Please enter your GhostWriter OpLog ID")
mythicEnv.Set("GHOSTWRITER_OPLOG_ID", gwID)
}
if !mythicEnv.IsSet("MYTHIC_API_KEY") {
mythicID := askVariable("Please enter your Mythic API Key (optional)")
mythicEnv.Set("MYTHIC_API_KEY", mythicID)
}
// just got new variables, need to update our .env
writeMythicEnvironmentVariables()
}
case "mythic_grafana":
pStruct["build"] = map[string]interface{}{
"context": "./grafana-docker",
"args": buildArguments,
}
pStruct["ports"] = []string{
"127.0.0.1:3000:3000",
}
pStruct["volumes"] = []string{
"./grafana-docker/storage:/var/lib/grafana",
}
pStruct["user"] = "root"
case "mythic_prometheus":
pStruct["build"] = map[string]interface{}{
"context": "./prometheus-docker",
"args": buildArguments,
}
pStruct["ports"] = []string{
"127.0.0.1:9090:9090",
}
pStruct["volumes"] = []string{
"./prometheus-docker/prometheus.yml:/etc/prometheus/prometheus.yml:ro",
}
case "mythic_postgres_exporter":
pStruct["build"] = map[string]interface{}{
"context": "./postgres-exporter-docker",
"args": buildArguments,
}
pStruct["ports"] = []string{
"127.0.0.1:9187:9187",
}
pStruct["volumes"] = []string{
"./postgres-exporter-docker/queries.yaml:/queries.yaml",
}
pStruct["links"] = []string{
"mythic_postgres",
"mythic_prometheus",
}
pStruct["environment"] = []string{
"DATA_SOURCE_NAME=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}?sslmode=disable",
}
pStruct["command"] = "--extend.query-path=/queries.yaml"
}
if !curConfig.IsSet("services." + strings.ToLower(service)) {
curConfig.Set("services."+strings.ToLower(service), pStruct)
fmt.Printf("[+] Added %s to docker-compose\n", strings.ToLower(service))
} else {
curConfig.Set("services."+strings.ToLower(service), pStruct)
}
if !curConfig.IsSet("networks") {
curConfig.Set("networks", networkInfo)
} else {
curConfig.Set("networks.default_network.driver_opts", map[string]string{
"com.docker.network.bridge.name": "mythic_if",
})
}
curConfig.WriteConfig()
}
func removeMythicServiceDockerComposeEntry(service string) {
var curConfig = viper.New()
curConfig.SetConfigName("docker-compose")
curConfig.SetConfigType("yaml")
curConfig.AddConfigPath(getCwdFromExe())
if err := curConfig.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Fatalf("[-] Error while reading in docker-compose file: %s\n", err)
} else {
log.Fatalf("[-] Error while parsing docker-compose file: %s\n", err)
}
}
networkInfo := map[string]interface{}{
"default_network": map[string]interface{}{
"driver": "bridge",
"driver_opts": map[string]string{
"com.docker.network.bridge.name": "mythic_if",
},
"ipam": map[string]interface{}{
"config": []map[string]interface{}{
{
"subnet": "172.100.0.0/16",
},
},
"driver": "default",
},
"labels": []string{
"mythic_network",
"default_network",
},
},
}
if !curConfig.IsSet("networks") {
// don't blow away changes to the network configuration
curConfig.Set("networks", networkInfo)
} else {
curConfig.Set("networks.default_network.driver_opts", map[string]string{
"com.docker.network.bridge.name": "mythic_if",
})
}
if isServiceRunning(service) {
DockerStop([]string{strings.ToLower(service)})
}
if curConfig.IsSet("services." + strings.ToLower(service)) {
delete(curConfig.Get("services").(map[string]interface{}), strings.ToLower(service))
fmt.Printf("[+] Removed %s from docker-compose because it's running on a different host\n", strings.ToLower(service))
}
if !curConfig.IsSet("networks") {
curConfig.Set("networks", networkInfo)
} else {
curConfig.Set("networks.default_network.driver_opts", map[string]string{
"com.docker.network.bridge.name": "mythic_if",
})
}
curConfig.WriteConfig()
}
func AddDockerComposeEntry(service string, additionalConfigs map[string]interface{}) error {
// add c2/payload [name] as type [group] to the main yaml file
var curConfig = viper.New()
curConfig.SetConfigName("docker-compose")
curConfig.SetConfigType("yaml")
curConfig.AddConfigPath(getCwdFromExe())
if err := curConfig.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Fatalf("[-] Error while reading in docker-compose file: %s", err)
} else {
log.Fatalf("[-] Error while parsing docker-compose file: %s", err)
}
}
networkInfo := map[string]interface{}{
"default_network": map[string]interface{}{
"driver": "bridge",
"driver_opts": map[string]string{
"com.docker.network.bridge.name": "mythic_if",
},
"ipam": map[string]interface{}{
"config": []map[string]interface{}{
{
"subnet": "172.100.0.0/16",
},
},
"driver": "default",
},
"labels": []string{
"mythic_network",
"default_network",
},
},
}
if !curConfig.IsSet("networks") {
curConfig.Set("networks", networkInfo)
} else {
curConfig.Set("networks.default_network.driver_opts", map[string]string{
"com.docker.network.bridge.name": "mythic_if",
})
}
if absPath, err := filepath.Abs(filepath.Join(getCwdFromExe(), InstalledServicesFolder, service)); err != nil {
fmt.Printf("[-] Failed to get the absolute path to the %s folder, does the folder exist?", InstalledServicesFolder)
fmt.Printf("[*] If the service doesn't exist, you might need to install with 'mythic-cli install'")
os.Exit(1)
} else if !dirExists(absPath) {
fmt.Printf("[-] %s does not exist, not adding to Mythic\n", absPath)
os.Exit(1)
} else {
pStruct := map[string]interface{}{
"labels": map[string]string{
"name": service,
},
"image": strings.ToLower(service),
"hostname": service,
"logging": map[string]interface{}{
"driver": "json-file",
"options": map[string]string{
"max-file": "1",
"max-size": "10m",
},
},
"restart": "always",
"volumes": []string{
absPath + ":/Mythic/",
},
"container_name": strings.ToLower(service),
//"networks": []string{
// "default_network",
//},
"cpus": mythicEnv.GetInt("INSTALLED_SERVICE_CPUS"),
}
for key, element := range additionalConfigs {
pStruct[key] = element
}
pStruct["build"] = map[string]interface{}{
"context": absPath,
"args": buildArguments,
}
pStruct["network_mode"] = "host"
pStruct["extra_hosts"] = []string{
"mythic_server:127.0.0.1",
"mythic_rabbitmq:127.0.0.1",
}
environment := []string{
"MYTHIC_ADDRESS=http://${MYTHIC_SERVER_HOST}:${MYTHIC_SERVER_PORT}/api/v1.4/agent_message",
"MYTHIC_WEBSOCKET=ws://${MYTHIC_SERVER_HOST}:${MYTHIC_SERVER_PORT}/ws/agent_message",
"RABBITMQ_USER=${RABBITMQ_USER}",
"RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD}",
"RABBITMQ_PORT=${RABBITMQ_PORT}",
"RABBITMQ_HOST=${RABBITMQ_HOST}",
"MYTHIC_SERVER_HOST=${MYTHIC_SERVER_HOST}",
"MYTHIC_SERVER_PORT=${MYTHIC_SERVER_PORT}",
"MYTHIC_SERVER_GRPC_PORT=${MYTHIC_SERVER_GRPC_PORT}",
"DEBUG_LEVEL=${DEBUG_LEVEL}",
}
if _, ok := pStruct["environment"]; ok {
pStruct["environment"] = updateEnvironmentVariables(curConfig.GetStringSlice("services."+strings.ToLower(service)+".environment"), environment)
} else {
pStruct["environment"] = environment
}
curConfig.Set("services."+strings.ToLower(service), pStruct)
if !curConfig.IsSet("networks") {
curConfig.Set("networks", networkInfo)
} else {
curConfig.Set("networks.default_network.driver_opts", map[string]string{
"com.docker.network.bridge.name": "mythic_if",
})
}
curConfig.WriteConfig()
fmt.Println("[+] Successfully updated docker-compose.yml")
}
return nil
}
func RemoveDockerComposeEntry(service string) error {
// add c2/payload [name] as type [group] to the main yaml file
var curConfig = viper.New()
curConfig.SetConfigName("docker-compose")
curConfig.SetConfigType("yaml")
curConfig.AddConfigPath(getCwdFromExe())
if err := curConfig.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Fatalf("[-] Error while reading in docker-compose file: %s", err)
} else {
log.Fatalf("[-] Error while parsing docker-compose file: %s", err)
}
}
networkInfo := map[string]interface{}{
"default_network": map[string]interface{}{
"driver": "bridge",
"driver_opts": map[string]string{
"com.docker.network.bridge.name": "mythic_if",
},
"ipam": map[string]interface{}{
"config": []map[string]interface{}{
{
"subnet": "172.100.0.0/16",
},
},
"driver": "default",
},
"labels": []string{
"mythic_network",
"default_network",
},
},
}
if !curConfig.IsSet("networks") {
curConfig.Set("networks", networkInfo)
} else {
curConfig.Set("networks.default_network.driver_opts", map[string]string{
"com.docker.network.bridge.name": "mythic_if",
})
}
if !stringInSlice(service, MythicPossibleServices) {
if isServiceRunning(service) {
DockerStop([]string{strings.ToLower(service)})
}
delete(curConfig.Get("services").(map[string]interface{}), strings.ToLower(service))
fmt.Printf("[+] Removed %s from docker-compose\n", strings.ToLower(service))
}
curConfig.WriteConfig()
fmt.Println("[+] Successfully updated docker-compose.yml")
if !curConfig.IsSet("networks") {
curConfig.Set("networks", networkInfo)
} else {
curConfig.Set("networks.default_network.driver_opts", map[string]string{
"com.docker.network.bridge.name": "mythic_if",
})
}
curConfig.WriteConfig()
return nil
}
func runDockerCompose(args []string) error {
lookPath, err := exec.LookPath("docker-compose")
if err != nil {
lookPath, err = exec.LookPath("docker")
if err != nil {
log.Fatalf("[-] docker-compose and docker are not installed or available in the current PATH")
} else {
// adjust the current args for docker compose subcommand
args = append([]string{"compose"}, args...)
}
}
exe, err := os.Executable()
if err != nil {
log.Fatalf("[-] Failed to get lookPath to current executable")
}
exePath := filepath.Dir(exe)
command := exec.Command(lookPath, args...)
command.Dir = exePath
command.Env = getMythicEnvList()
stdout, err := command.StdoutPipe()
if err != nil {
log.Fatalf("[-] Failed to get stdout pipe for running docker-compose")
}
stderr, err := command.StderrPipe()
if err != nil {
log.Fatalf("[-] Failed to get stderr pipe for running docker-compose")
}
stdoutScanner := bufio.NewScanner(stdout)
stderrScanner := bufio.NewScanner(stderr)
go func() {
for stdoutScanner.Scan() {
fmt.Printf("%s\n", stdoutScanner.Text())
}
}()
go func() {
for stderrScanner.Scan() {
fmt.Printf("%s\n", stderrScanner.Text())
}
}()
err = command.Start()
if err != nil {
log.Fatalf("[-] Error trying to start docker-compose: %v\n", err)
}
err = command.Wait()
if err != nil {
fmt.Printf("[-] Error from docker-compose: %v\n", err)
return err
}
return nil
}
func GetAllExistingNonMythicServiceNames() ([]string, error) {
// get all services that exist within the loaded config
groupNameConfig := viper.New()
groupNameConfig.SetConfigName("docker-compose")
groupNameConfig.SetConfigType("yaml")
groupNameConfig.AddConfigPath(getCwdFromExe())
if err := groupNameConfig.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
fmt.Printf("[-] Error while reading in docker-compose file: %s", err)
return []string{}, err
} else {
fmt.Printf("[-] Error while parsing docker-compose file: %s", err)
return []string{}, err
}
}
servicesSub := groupNameConfig.Sub("services")
services := servicesSub.AllSettings()
containerList := []string{}
for service := range services {
if !stringInSlice(service, MythicPossibleServices) {
containerList = append(containerList, service)
}
}
return containerList, nil
}
func GetCurrentMythicServiceNames() ([]string, error) {
groupNameConfig := viper.New()
groupNameConfig.SetConfigName("docker-compose")
groupNameConfig.SetConfigType("yaml")
groupNameConfig.AddConfigPath(getCwdFromExe())
if err := groupNameConfig.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
fmt.Printf("[-] Error while reading in docker-compose file: %s", err)
return []string{}, err
} else {
fmt.Printf("[-] Error while parsing docker-compose file: %s", err)
return []string{}, err
}
}
servicesSub := groupNameConfig.Sub("services")
services := servicesSub.AllSettings()
containerList := []string{}
for service := range services {
if stringInSlice(service, MythicPossibleServices) {
containerList = append(containerList, service)
}
}
return containerList, nil
}
func getBuildArguments() []string {
var buildEnv = viper.New()
buildEnv.SetConfigName("build.env")
buildEnv.SetConfigType("env")
buildEnv.AddConfigPath(getCwdFromExe())
buildEnv.AutomaticEnv()
if !fileExists(filepath.Join(getCwdFromExe(), "build.env")) {
fmt.Printf("[*] No build.env file detected in Mythic's root directory; not supplying build arguments to docker containers\n")
fmt.Printf(" If you need to supply build arguments to docker containers, create build.env and supply key=value entries there\n")
return []string{}
}
if err := buildEnv.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Fatalf("[-] Error while reading in build.env file: %s", err)
} else {
log.Fatalf("[-]Error while parsing build.env file: %s", err)
}
}
c := buildEnv.AllSettings()
// to make it easier to read and look at, get all the keys, sort them, and display variables in order
keys := make([]string, 0, len(c))
for k := range c {
keys = append(keys, k)
}
sort.Strings(keys)
var args []string
for _, key := range keys {
args = append(args, fmt.Sprintf("%s=%s", strings.ToUpper(key), buildEnv.GetString(key)))
}
return args
}
func getMythicEnvList() []string {
env := mythicEnv.AllSettings()
var envList []string
for key := range env {
val := mythicEnv.GetString(key)
if val != "" {
// prevent trying to append arrays or dictionaries to our environment list
//fmt.Println(strings.ToUpper(key), val)
envList = append(envList, strings.ToUpper(key)+"="+val)
}
}
envList = append(envList, os.Environ()...)
return envList
}
func CheckDockerCompose() {
groupNameConfig := viper.New()
groupNameConfig.SetConfigName("docker-compose")
groupNameConfig.SetConfigType("yaml")
groupNameConfig.AddConfigPath(getCwdFromExe())
if err := groupNameConfig.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
fmt.Printf("[-] Error while reading in docker-compose file: %s\n", err)
if _, err := os.Create("docker-compose.yml"); err != nil {
fmt.Printf("[-] Failed to create docker-compose.yml file: %v\n", err)
os.Exit(1)
} else {
if err := groupNameConfig.ReadInConfig(); err != nil {
fmt.Printf("[-] Failed to read in new docker-compose.yml file: %v\n", err)
os.Exit(1)
} else {
fmt.Printf("[+] Successfully created new docker-compose.yml file. Populating it now...\n")
intendedMythicContainers, _ := GetIntendedMythicServiceNames()
for _, container := range intendedMythicContainers {
addMythicServiceDockerComposeEntry(container)
}
return
}
}
} else {
fmt.Printf("[-] Error while parsing docker-compose file: %s", err)
os.Exit(1)
}
}
servicesSub := groupNameConfig.Sub("services")
if servicesSub == nil {
intendedMythicContainers, _ := GetIntendedMythicServiceNames()
for _, container := range intendedMythicContainers {
addMythicServiceDockerComposeEntry(container)
}
}
}
+220
View File
@@ -0,0 +1,220 @@
package internal
import (
"fmt"
"github.com/spf13/viper"
"log"
"os"
"path/filepath"
"sort"
"strings"
)
var mythicEnv = viper.New()
func setMythicConfigDefaultValues() {
// nginx configuration
mythicEnv.SetDefault("nginx_port", 7443)
mythicEnv.SetDefault("nginx_host", "mythic_nginx")
mythicEnv.SetDefault("nginx_bind_localhost_only", false)
mythicEnv.SetDefault("nginx_use_ssl", true)
// mythic react UI configuration
mythicEnv.SetDefault("mythic_react_host", "mythic_react")
mythicEnv.SetDefault("mythic_react_port", 3000)
mythicEnv.SetDefault("mythic_react_bind_localhost_only", true)
// mythic server configuration
mythicEnv.SetDefault("documentation_host", "mythic_documentation")
mythicEnv.SetDefault("documentation_port", 8090)
mythicEnv.SetDefault("documentation_bind_localhost_only", true)
mythicEnv.SetDefault("debug_level", "warning")
mythicEnv.SetDefault("mythic_debug_agent_message", false)
mythicEnv.SetDefault("mythic_server_port", 17443)
mythicEnv.SetDefault("mythic_server_grpc_port", 17444)
mythicEnv.SetDefault("mythic_server_host", "mythic_server")
mythicEnv.SetDefault("mythic_server_bind_localhost_only", true)
mythicEnv.SetDefault("mythic_server_dynamic_ports", "7000-7010")
mythicEnv.SetDefault("mythic_server_command", "/mythic_server.bin")
// postgres configuration
mythicEnv.SetDefault("postgres_host", "mythic_postgres")
mythicEnv.SetDefault("postgres_port", 5432)
mythicEnv.SetDefault("postgres_bind_localhost_only", true)
mythicEnv.SetDefault("postgres_db", "mythic_db")
mythicEnv.SetDefault("postgres_user", "mythic_user")
mythicEnv.SetDefault("postgres_password", generateRandomPassword(30))
mythicEnv.SetDefault("postgres_cpus", "2")
// rabbitmq configuration
mythicEnv.SetDefault("rabbitmq_host", "mythic_rabbitmq")
mythicEnv.SetDefault("rabbitmq_port", 5672)
mythicEnv.SetDefault("rabbitmq_bind_localhost_only", true)
mythicEnv.SetDefault("rabbitmq_user", "mythic_user")
mythicEnv.SetDefault("rabbitmq_password", generateRandomPassword(30))
mythicEnv.SetDefault("rabbitmq_vhost", "mythic_vhost")
mythicEnv.SetDefault("rabbitmq_cpus", "2")
// jwt configuration
mythicEnv.SetDefault("jwt_secret", generateRandomPassword(30))
// hasura configuration
mythicEnv.SetDefault("hasura_host", "mythic_graphql")
mythicEnv.SetDefault("hasura_port", 8080)
mythicEnv.SetDefault("hasura_bind_localhost_only", true)
mythicEnv.SetDefault("hasura_secret", generateRandomPassword(30))
mythicEnv.SetDefault("hasura_cpus", "2")
// docker-compose configuration
mythicEnv.SetDefault("COMPOSE_PROJECT_NAME", "mythic")
mythicEnv.SetDefault("REBUILD_ON_START", false)
// Mythic instance configuration
mythicEnv.SetDefault("mythic_admin_user", "mythic_admin")
mythicEnv.SetDefault("mythic_admin_password", generateRandomPassword(30))
mythicEnv.SetDefault("default_operation_name", "Operation Chimera")
mythicEnv.SetDefault("allowed_ip_blocks", "0.0.0.0/0")
// jupyter configuration
mythicEnv.SetDefault("jupyter_port", 8888)
mythicEnv.SetDefault("jupyter_host", "mythic_jupyter")
mythicEnv.SetDefault("jupyter_bind_localhost_only", true)
// debugging help
mythicEnv.SetDefault("postgres_debug", false)
// installed service configuration
mythicEnv.SetDefault("installed_service_cpus", "1")
}
func parseMythicEnvironmentVariables() {
setMythicConfigDefaultValues()
mythicEnv.SetConfigName(".env")
mythicEnv.SetConfigType("env")
mythicEnv.AddConfigPath(getCwdFromExe())
mythicEnv.AutomaticEnv()
if !fileExists(filepath.Join(getCwdFromExe(), ".env")) {
_, err := os.Create(filepath.Join(getCwdFromExe(), ".env"))
if err != nil {
log.Fatalf("[-] .env doesn't exist and couldn't be created")
}
}
if err := mythicEnv.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Fatalf("[-] Error while reading in .env file: %s", err)
} else {
log.Fatalf("[-]Error while parsing .env file: %s", err)
}
}
portChecks := map[string][]string{
"MYTHIC_SERVER_HOST": {
"MYTHIC_SERVER_PORT",
"mythic_server",
},
"POSTGRES_HOST": {
"POSTGRES_PORT",
"mythic_postgres",
},
"HASURA_HOST": {
"HASURA_PORT",
"mythic_graphql",
},
"RABBITMQ_HOST": {
"RABBITMQ_PORT",
"mythic_rabbitmq",
},
"DOCUMENTATION_HOST": {
"DOCUMENTATION_PORT",
"mythic_documentation",
},
"NGINX_HOST": {
"NGINX_PORT",
"mythic_nginx",
},
"MYTHIC_REACT_HOST": {
"MYTHIC_REACT_PORT",
"mythic_react",
},
"MYTHIC_JUPYTER_HOST": {
"MYTHIC_JUPYTER_PORT",
"mythic_jupyter",
},
}
for key, val := range portChecks {
if mythicEnv.GetString(key) == "127.0.0.1" {
mythicEnv.Set(key, val[1])
}
}
writeMythicEnvironmentVariables()
if !mythicEnv.GetBool("postgres_debug") {
// update the MythicPossibleServices to not include the two debugging services of grafana and postgres_exporter
MythicPossibleServices = []string{
"mythic_postgres",
"mythic_react",
"mythic_server",
"mythic_nginx",
"mythic_rabbitmq",
"mythic_graphql",
"mythic_documentation",
"mythic_jupyter",
"mythic_sync",
}
}
}
func writeMythicEnvironmentVariables() {
c := mythicEnv.AllSettings()
// to make it easier to read and look at, get all the keys, sort them, and display variables in order
keys := make([]string, 0, len(c))
for k := range c {
keys = append(keys, k)
}
sort.Strings(keys)
f, err := os.Create(filepath.Join(getCwdFromExe(), ".env"))
if err != nil {
log.Fatalf("[-] Error writing out environment!\n%v", err)
}
defer f.Close()
for _, key := range keys {
if len(mythicEnv.GetString(key)) == 0 {
_, err = f.WriteString(fmt.Sprintf("%s=\n", strings.ToUpper(key)))
} else {
_, err = f.WriteString(fmt.Sprintf("%s=\"%s\"\n", strings.ToUpper(key), mythicEnv.GetString(key)))
}
if err != nil {
log.Fatalf("[-] Failed to write out environment!\n%v", err)
}
}
return
}
func GetConfigAllStrings() map[string]string {
c := mythicEnv.AllSettings()
// to make it easier to read and look at, get all the keys, sort them, and display variables in order
keys := make([]string, 0)
for k := range c {
keys = append(keys, k)
}
resultMap := make(map[string]string)
for _, key := range keys {
resultMap[key] = mythicEnv.GetString(key)
}
return resultMap
}
func GetConfigStrings(args []string) map[string]string {
resultMap := make(map[string]string)
for i := 0; i < len(args[0:]); i++ {
setting := strings.ToLower(args[i])
val := mythicEnv.GetString(setting)
if val == "" {
log.Fatalf("Config variable `%s` not found", setting)
} else {
resultMap[setting] = val
}
}
return resultMap
}
func SetConfigStrings(key string, value string) {
if strings.ToLower(value) == "true" {
mythicEnv.Set(key, true)
} else if strings.ToLower(value) == "false" {
mythicEnv.Set(key, false)
} else {
mythicEnv.Set(key, value)
}
mythicEnv.Get(key)
writeMythicEnvironmentVariables()
}
func Initialize() {
parseMythicEnvironmentVariables()
writeMythicEnvironmentVariables()
CheckDockerCompose()
}
+79
View File
@@ -0,0 +1,79 @@
package internal
import (
"bufio"
"fmt"
"os"
"os/exec"
"path/filepath"
)
func runGitClone(args []string) error {
if lookPath, err := exec.LookPath("git"); err != nil {
fmt.Printf("[-] git is not installed or not available in the current PATH variable")
return err
} else if exe, err := os.Executable(); err != nil {
fmt.Printf("[-] Failed to get lookPath to current executable")
return err
} else {
exePath := filepath.Dir(exe)
// git -c http.sslVerify=false clone --recurse-submodules --single-branch --branch $2 $1 temp
command := exec.Command(lookPath, args...)
command.Dir = exePath
command.Env = getMythicEnvList()
if stdout, err := command.StdoutPipe(); err != nil {
fmt.Printf("[-] Failed to get stdout pipe for running git")
return err
} else if stderr, err := command.StderrPipe(); err != nil {
fmt.Printf("[-] Failed to get stderr pipe for running git")
return err
} else {
stdoutScanner := bufio.NewScanner(stdout)
stderrScanner := bufio.NewScanner(stderr)
go func() {
for stdoutScanner.Scan() {
fmt.Printf("%s\n", stdoutScanner.Text())
}
}()
go func() {
for stderrScanner.Scan() {
fmt.Printf("%s\n", stderrScanner.Text())
}
}()
if err = command.Start(); err != nil {
fmt.Printf("[-] Error trying to start git: %v\n", err)
return err
} else if err = command.Wait(); err != nil {
fmt.Printf("[-] Error trying to run git: %v\n", err)
return err
}
}
}
return nil
}
func runGitLsRemote(args []string) (err error) {
if lookPath, err := exec.LookPath("git"); err != nil {
fmt.Printf("[-] git is not installed or not available in the current PATH variable")
return err
} else if exe, err := os.Executable(); err != nil {
fmt.Printf("[-] Failed to get lookPath to current executable")
return err
} else {
exePath := filepath.Dir(exe)
// git -c http.sslVerify=false clone --recurse-submodules --single-branch --branch $2 $1 temp
// git ls-remote URL HEAD
command := exec.Command(lookPath, args...)
command.Dir = exePath
command.Env = getMythicEnvList()
command.Env = append(command.Env, "GIT_TERMINAL_PROMPT=0")
if err = command.Run(); err != nil {
fmt.Printf("[-] Error trying to start git: %v\n", err)
return err
} else {
return nil
}
}
}
@@ -0,0 +1,439 @@
package internal
import (
"fmt"
"github.com/spf13/viper"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
)
func InstallFolder(installPath string, overWrite bool) error {
workingPath := getCwdFromExe()
if fileExists(filepath.Join(installPath, "config.json")) {
var config = viper.New()
config.SetConfigName("config")
config.SetConfigType("json")
fmt.Printf("[*] Parsing config.json\n")
config.AddConfigPath(installPath)
if err := config.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
fmt.Printf("[-] Error while reading in config file: %s", err)
return err
} else {
fmt.Printf("[-] Error while parsing config file: %s", err)
return err
}
}
if !config.GetBool("exclude_payload_type") {
// handle the payload type copying here
files, err := ioutil.ReadDir(filepath.Join(installPath, "Payload_Type"))
if err != nil {
fmt.Printf("[-] Failed to list contents of new Payload_Type folder: %v\n", err)
return err
}
for _, f := range files {
if f.IsDir() {
fmt.Printf("[*] Processing Payload Type %s\n", f.Name())
if dirExists(filepath.Join(workingPath, InstalledServicesFolder, f.Name())) {
if overWrite || askConfirm("[*] "+f.Name()+" already exists. Replace current version? ") {
fmt.Printf("[*] Stopping current container\n")
if isServiceRunning(strings.ToLower(f.Name())) {
DockerStop([]string{f.Name()})
}
fmt.Printf("[*] Removing current version\n")
err = os.RemoveAll(filepath.Join(workingPath, InstalledServicesFolder, f.Name()))
if err != nil {
fmt.Printf("[-] Failed to remove current version: %v\n", err)
fmt.Printf("[-] Continuing to the next payload\n")
continue
} else {
fmt.Printf("[+] Successfully removed the current version\n")
}
} else {
fmt.Printf("[!] Skipping Payload Type, %s\n", f.Name())
continue
}
}
fmt.Printf("[*] Copying new version of payload into place\n")
err = copyDir(filepath.Join(installPath, "Payload_Type", f.Name()), filepath.Join(workingPath, InstalledServicesFolder, f.Name()))
if err != nil {
fmt.Printf("[-] Failed to copy directory over: %v\n", err)
continue
}
fmt.Printf("[*] Adding service into docker-compose\n")
if config.IsSet("docker-compose") {
AddDockerComposeEntry(f.Name(), config.GetStringMap("docker-compose"))
} else {
AddDockerComposeEntry(f.Name(), make(map[string]interface{}))
}
}
}
fmt.Printf("[+] Successfully installed service\n")
} else {
fmt.Printf("[*] Skipping over Payload Type\n")
}
if !config.GetBool("exclude_c2_profiles") {
// handle the c2 profile copying here
files, err := ioutil.ReadDir(filepath.Join(installPath, "C2_Profiles"))
if err != nil {
fmt.Printf("[-] Failed to list contents of C2_Profiles folder from clone\n")
return err
}
for _, f := range files {
if f.IsDir() {
fmt.Printf("[*] Processing C2 Profile %s\n", f.Name())
if dirExists(filepath.Join(workingPath, InstalledServicesFolder, f.Name())) {
if overWrite || askConfirm("[*] "+f.Name()+" already exists. Replace current version? ") {
fmt.Printf("[*] Stopping current container\n")
if isServiceRunning(strings.ToLower(f.Name())) {
DockerStop([]string{f.Name()})
}
fmt.Printf("[*] Removing current version\n")
err = os.RemoveAll(filepath.Join(workingPath, InstalledServicesFolder, f.Name()))
if err != nil {
fmt.Printf("[-] Failed to remove current version: %v\n", err)
fmt.Printf("[-] Continuing to the next c2 profile\n")
continue
} else {
fmt.Printf("[+] Successfully removed the current version\n")
}
} else {
fmt.Printf("[!] Skipping C2 Profile, %s\n", f.Name())
continue
}
}
fmt.Printf("[*] Copying new version into place\n")
err = copyDir(filepath.Join(installPath, "C2_Profiles", f.Name()), filepath.Join(workingPath, InstalledServicesFolder, f.Name()))
if err != nil {
fmt.Printf("[-] Failed to copy directory over\n")
continue
}
// need to make sure the c2_service.sh file is executable
/*
if fileExists(filepath.Join(workingPath, "C2_Profiles", f.Name(), "mythic", "c2_service.sh")) {
err = os.Chmod(filepath.Join(workingPath, "C2_Profiles", f.Name(), "mythic", "c2_service.sh"), 0777)
if err != nil {
fmt.Printf("[-] Failed to make c2_service.sh file executable\n")
continue
}
} else {
fmt.Printf("[-] failed to find c2_service file for %s\n", f.Name())
continue
}
*/
// now add payload type to yaml config
fmt.Printf("[*] Adding c2, %s, into docker-compose\n", f.Name())
AddDockerComposeEntry(f.Name(), make(map[string]interface{}))
}
}
fmt.Printf("[+] Successfully installed c2\n")
} else {
fmt.Printf("[*] Skipping over C2 Profile\n")
}
if !config.GetBool("exclude_documentation_payload") {
// handle payload documentation copying here
files, err := ioutil.ReadDir(filepath.Join(installPath, "documentation-payload"))
if err != nil {
fmt.Printf("[-] Failed to list contents of documentation_payload folder from clone\n")
return err
}
for _, f := range files {
if f.IsDir() {
fmt.Printf("[*] Processing Documentation for %s\n", f.Name())
if dirExists(filepath.Join(workingPath, "documentation-docker", "content", "Agents", f.Name())) {
if overWrite || askConfirm("[*] "+f.Name()+" documentation already exists. Replace current version? ") {
fmt.Printf("[*] Removing current version\n")
err = os.RemoveAll(filepath.Join(workingPath, "documentation-docker", "content", "Agents", f.Name()))
if err != nil {
fmt.Printf("[-] Failed to remove current version: %v\n", err)
fmt.Printf("[-] Continuing to the next payload documentation\n")
continue
} else {
fmt.Printf("[+] Successfully removed the current version\n")
}
} else {
fmt.Printf("[!] Skipping documentation for , %s\n", f.Name())
continue
}
}
fmt.Printf("[*] Copying new documentation into place\n")
err = copyDir(filepath.Join(installPath, "documentation-payload", f.Name()), filepath.Join(workingPath, "documentation-docker", "content", "Agents", f.Name()))
if err != nil {
fmt.Printf("[-] Failed to copy directory over\n")
continue
}
}
}
fmt.Printf("[+] Successfully installed Payload documentation\n")
} else {
fmt.Printf("[*] Skipping over Payload Documentation\n")
}
if !config.GetBool("exclude_documentation_c2") {
// handle the c2 documentation copying here
files, err := ioutil.ReadDir(filepath.Join(installPath, "documentation-c2"))
if err != nil {
fmt.Printf("[-] Failed to list contents of documentation_payload folder from clone")
return err
}
for _, f := range files {
if f.IsDir() {
fmt.Printf("[*] Processing Documentation for %s\n", f.Name())
if dirExists(filepath.Join(workingPath, "documentation-docker", "content", "C2 Profiles", f.Name())) {
if overWrite || askConfirm("[*] "+f.Name()+" documentation already exists. Replace current version? ") {
fmt.Printf("[*] Removing current version\n")
err = os.RemoveAll(filepath.Join(workingPath, "documentation-docker", "content", "C2 Profiles", f.Name()))
if err != nil {
fmt.Printf("[-] Failed to remove current version: %v\n", err)
fmt.Printf("[-] Continuing to the next c2 documentation\n")
continue
} else {
fmt.Printf("[+] Successfully removed the current version\n")
}
} else {
fmt.Printf("[!] Skipping documentation for %s\n", f.Name())
continue
}
}
fmt.Printf("[*] Copying new documentation version into place\n")
err = copyDir(filepath.Join(installPath, "documentation-c2", f.Name()), filepath.Join(workingPath, "documentation-docker", "content", "C2 Profiles", f.Name()))
if err != nil {
fmt.Printf("[-] Failed to copy directory over\n")
continue
}
}
}
fmt.Printf("[+] Successfully installed c2 documentation\n")
} else {
fmt.Printf("[*] Skipping over C2 Documentation\n")
}
if isServiceRunning("mythic_documentation") {
fmt.Printf("[*] Restarting mythic_documentation container to pull in changes\n")
DockerStop([]string{"mythic_documentation"})
DockerStart([]string{"mythic_documentation"})
}
} else {
log.Fatal("[-] Failed to find config.json in cloned down repo\n")
}
return nil
}
func InstallService(url string, branch string, overWrite bool) error {
// make our temp directory to clone into
workingPath := getCwdFromExe()
fmt.Printf("[*] Creating temporary directory\n")
if dirExists(filepath.Join(workingPath, "tmp")) {
err := os.RemoveAll(filepath.Join(workingPath, "tmp"))
if err != nil {
fmt.Printf("[-] tmp directory couldn't be deleted for a fresh install: %v\n", err)
return err
}
}
err := os.Mkdir(filepath.Join(workingPath, "tmp"), 0755)
defer os.RemoveAll(filepath.Join(workingPath, "tmp"))
if err != nil {
fmt.Printf("[-] Failed to make temp directory for cloning: %v\n", err)
return err
}
if branch == "" {
fmt.Printf("[*] Cloning %s\n", url)
err = runGitClone([]string{"-c", "http.sslVerify=false", "clone", "--recurse-submodules", "--single-branch", url, filepath.Join(workingPath, "tmp")})
} else {
fmt.Printf("[*] Cloning branch \"%s\" from %s\n", branch, url)
err = runGitClone([]string{"-c", "http.sslVerify=false", "clone", "--recurse-submodules", "--single-branch", "--branch", branch, url, filepath.Join(workingPath, "tmp")})
}
if err != nil {
fmt.Printf("[-] Failed to clone down repository: %v\n", err)
return err
}
if err = InstallFolder(filepath.Join(workingPath, "tmp"), overWrite); err != nil {
fmt.Printf("[-] Failed to install: %v\n", err)
return err
} else {
return nil
}
}
func installServiceByName(service string) error {
// just have a service name, check MythicAgents, MythicC2Profiles, or error
agentURL := fmt.Sprintf("https://github.com/MythicAgents/%s", service)
c2URL := fmt.Sprintf("https://github.com/MythicC2Profiles/%s", service)
if err := runGitLsRemote([]string{"lfs-remote", agentURL, "HEAD"}); err != nil {
if err := runGitLsRemote([]string{"lfs-remote", c2URL, "HEAD"}); err != nil {
fmt.Printf("[-] Failed to find an agent or c2 profile by that name")
return err
} else {
// this exists as a c2 profile repo, so we can pull that down
return InstallService(c2URL, "", true)
}
} else {
// this exists as an agent repo, so we can pull that down
return InstallService(agentURL, "", true)
}
}
func InstallMythicSyncFolder(installPath string) error {
workingPath := getCwdFromExe()
viper.SetConfigName("docker-compose")
viper.SetConfigType("yaml")
viper.AddConfigPath(getCwdFromExe())
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
fmt.Printf("[-] Error while reading in docker-compose file: %s\n", err)
return err
} else {
fmt.Printf("[-] Error while parsing docker-compose file: %s\n", err)
return err
}
}
service := "mythic_sync"
if isServiceRunning(service) {
DockerStop([]string{service})
}
if dirExists(filepath.Join(workingPath, InstalledServicesFolder, service)) {
err := os.RemoveAll(filepath.Join(workingPath, InstalledServicesFolder, service))
if err != nil {
fmt.Printf("[-] %s directory couldn't be deleted for a fresh install: %v\n", filepath.Join(workingPath, InstalledServicesFolder, service), err)
return err
}
}
if err := copyDir(installPath, filepath.Join(workingPath, InstalledServicesFolder, service)); err != nil {
fmt.Printf("[-] Failed to create %s directory to install mythic_sync: %v\n", service, err)
return err
}
addMythicServiceDockerComposeEntry(service)
fmt.Printf("[+] Successfully installed mythic_sync!\n")
if isServiceRunning("mythic_server") {
DockerStart([]string{strings.ToLower(service)})
}
return nil
}
func InstallMythicSync(url string, branch string) error {
// make our temp directory to clone into
workingPath := getCwdFromExe()
fmt.Printf("[*] Creating temporary directory\n")
if dirExists(filepath.Join(workingPath, "tmp")) {
if err := os.RemoveAll(filepath.Join(workingPath, "tmp")); err != nil {
fmt.Printf("[-] %s directory couldn't be deleted for a fresh install: %v\n", filepath.Join(workingPath, "tmp"), err)
return err
}
}
err := os.Mkdir(filepath.Join(workingPath, "tmp"), 0755)
defer os.RemoveAll(filepath.Join(workingPath, "tmp"))
if err != nil {
log.Fatalf("[-] Failed to make temp directory for cloning: %v\n", err)
}
if branch == "" {
fmt.Printf("[*] Cloning %s\n", url)
err = runGitClone([]string{"-c", "http.sslVerify=false", "clone", "--recurse-submodules", "--single-branch", url, filepath.Join(workingPath, "tmp")})
} else {
fmt.Printf("[*] Cloning branch \"%s\" from %s\n", branch, url)
err = runGitClone([]string{"-c", "http.sslVerify=false", "clone", "--recurse-submodules", "--single-branch", "--branch", branch, url, filepath.Join(workingPath, "tmp")})
}
if err != nil {
fmt.Printf("[-] Failed to clone down repository: %v\n", err)
return err
}
return InstallMythicSyncFolder(filepath.Join(workingPath, "tmp"))
}
func UninstallMythicSync() {
workingPath := getCwdFromExe()
service := "mythic_sync"
if isServiceRunning(service) {
DockerStop([]string{service})
}
removeMythicServiceDockerComposeEntry(service)
if dirExists(filepath.Join(workingPath, InstalledServicesFolder, service)) {
err := os.RemoveAll(filepath.Join(workingPath, InstalledServicesFolder, service))
if err != nil {
log.Fatalf("[-] %s directory couldn't be deleted: %v\n", service, err)
} else {
fmt.Printf("[+] Successfully removed %s from disk\n", service)
}
} else {
fmt.Printf("[+] %s was not installed on disk\n", service)
}
fmt.Printf("[+] Successfully uninstalled mythic_sync\n")
}
func UninstallService(services []string) {
workingPath := getCwdFromExe()
for _, service := range services {
if stringInSlice(strings.ToLower(service), MythicPossibleServices) {
fmt.Printf("[-] Trying to uninstall Mythic services not allowed\n")
os.Exit(1)
}
found := false
if dirExists(filepath.Join(workingPath, InstalledServicesFolder, service)) {
fmt.Printf("[*] Stopping and removing container\n")
if isServiceRunning(strings.ToLower(service)) {
DockerStop([]string{strings.ToLower(service)})
}
fmt.Printf("[*] Removing %s from docker-compose\n", strings.ToLower(service))
RemoveDockerComposeEntry(strings.ToLower(service))
fmt.Printf("[*] Removing Payload Type folder from disk\n")
found = true
err := os.RemoveAll(filepath.Join(workingPath, InstalledServicesFolder, service))
if err != nil {
fmt.Printf("[-] Failed to remove folder: %v\n", err)
os.Exit(1)
} else {
fmt.Printf("[+] Successfully removed %s's folder\n", service)
}
if dirExists(filepath.Join(workingPath, "documentation-docker", "content", "Agents", service)) {
fmt.Printf("[*] Removing Payload Type's Documentation from disk\n")
err = os.RemoveAll(filepath.Join(workingPath, "documentation-docker", "content", "Agents", service))
if err != nil {
fmt.Printf("[-] Failed to remove Payload Type's Documentation: %v\n", err)
os.Exit(1)
} else {
fmt.Printf("[+] Successfully removed Payload Type's Documentation\n")
}
}
if dirExists(filepath.Join(workingPath, "documentation-docker", "content", "C2 Profiles", service)) {
fmt.Printf("[*] Removing C2 Profile's Documentation\n")
err = os.RemoveAll(filepath.Join(workingPath, "documentation-docker", "content", "C2 Profiles", service))
if err != nil {
fmt.Printf("[-] Failed to remove C2 Profile's Documentation: %v\n", err)
os.Exit(1)
} else {
fmt.Printf("[+] Successfully removed C2 Profile's Documentation\n")
}
}
if dirExists(filepath.Join(workingPath, "documentation-docker", "content", "Wrappers", service)) {
fmt.Printf("[*] Removing C2 Profile's Documentation\n")
err = os.RemoveAll(filepath.Join(workingPath, "documentation-docker", "content", "Wrappers", service))
if err != nil {
fmt.Printf("[-] Failed to remove C2 Profile's Documentation: %v\n", err)
os.Exit(1)
} else {
fmt.Printf("[+] Successfully removed C2 Profile's Documentation\n")
}
}
if fileExists(filepath.Join(workingPath, "mythic-docker", "src", "static", service+".svg")) {
found = true
err := os.RemoveAll(filepath.Join(workingPath, "src", "static", service+".svg"))
if err != nil {
fmt.Printf("[-] Failed to agent icon: %v\n", err)
os.Exit(1)
} else {
fmt.Printf("[+] Successfully removed %s's old UI icon\n", service)
}
}
}
if found {
fmt.Printf("[+] Successfully Uninstalled %s\n", service)
if isServiceRunning("mythic_documentation") {
fmt.Printf("[*] Restarting mythic_documentation container to pull in changes\n")
DockerStop([]string{"mythic_documentation"})
DockerStart([]string{"mythic_documentation"})
}
return
} else {
fmt.Printf("[-] Failed to find any service folder by that name\n")
os.Exit(1)
}
}
}
+37
View File
@@ -0,0 +1,37 @@
package internal
import (
"fmt"
"log"
"os"
"path/filepath"
)
func DatabaseReset() {
fmt.Printf("[*] Stopping Mythic\n")
DockerStop([]string{})
workingPath := getCwdFromExe()
fmt.Printf("[*] Removing database files\n")
err := os.RemoveAll(filepath.Join(workingPath, "postgres-docker", "database"))
if err != nil {
fmt.Printf("[-] Failed to remove database files\n")
} else {
fmt.Printf("[+] Successfully reset datbase files\n")
}
}
func RabbitmqReset(explicitCall bool) {
if explicitCall {
fmt.Printf("[*] Stopping Mythic\n")
DockerStop([]string{})
fmt.Printf("[*] Removing rabbitmq files\n")
}
workingPath := getCwdFromExe()
err := os.RemoveAll(filepath.Join(workingPath, "rabbitmq-docker", "storage"))
if err != nil {
log.Fatalf("[-] Failed to reset rabbitmq files: %v\n", err)
} else {
if explicitCall {
fmt.Printf("[+] Successfully reset rabbitmq files\n")
}
}
}
+397
View File
@@ -0,0 +1,397 @@
package internal
import (
"context"
"crypto/tls"
"encoding/binary"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/streadway/amqp"
"log"
"net"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"text/tabwriter"
"time"
)
func imageExists(containerName string) bool {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Fatalf("Failed to get client in GetLogs: %v", err)
}
desiredImage := fmt.Sprintf("%v:latest", strings.ToLower(containerName))
images, err := cli.ImageList(context.Background(), types.ImageListOptions{All: true})
if err != nil {
log.Fatalf("Failed to get container list: %v", err)
}
for _, image := range images {
for _, name := range image.RepoTags {
if name == desiredImage {
return true
}
}
}
return false
}
func TestMythicConnection() {
webAddress := "127.0.0.1"
if mythicEnv.GetString("NGINX_HOST") == "mythic_nginx" {
if mythicEnv.GetBool("NGINX_USE_SSL") {
webAddress = "https://127.0.0.1"
} else {
webAddress = "http://127.0.0.1"
}
} else {
if mythicEnv.GetBool("NGINX_USE_SSL") {
webAddress = "https://" + mythicEnv.GetString("NGINX_HOST")
} else {
webAddress = "http://" + mythicEnv.GetString("NGINX_HOST")
}
}
maxCount := 10
sleepTime := int64(10)
count := make([]int, maxCount)
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
fmt.Printf("[*] Waiting for Mythic Server and Nginx to come online (Retry Count = %d)\n", maxCount)
for i := range count {
fmt.Printf("[*] Attempting to connect to Mythic UI at %s:%d, attempt %d/%d\n", webAddress, mythicEnv.GetInt("NGINX_PORT"), i+1, maxCount)
resp, err := http.Get(webAddress + ":" + strconv.Itoa(mythicEnv.GetInt("NGINX_PORT")))
if err != nil {
fmt.Printf("[-] Failed to make connection to host, retrying in %ds\n", sleepTime)
fmt.Printf("%v\n", err)
} else {
defer resp.Body.Close()
if resp.StatusCode == 200 || resp.StatusCode == 404 {
fmt.Printf("[+] Successfully connected to Mythic at " + webAddress + ":" + strconv.Itoa(mythicEnv.GetInt("NGINX_PORT")) + "\n\n")
return
} else if resp.StatusCode == 502 || resp.StatusCode == 504 {
fmt.Printf("[-] Nginx is up, but waiting for Mythic Server, retrying connection in %ds\n", sleepTime)
} else {
fmt.Printf("[-] Connection failed with HTTP Status Code %d, retrying in %ds\n", resp.StatusCode, sleepTime)
}
}
time.Sleep(time.Duration(sleepTime) * time.Second)
}
fmt.Printf("[-] Failed to make connection to Mythic Server\n")
fmt.Printf(" This could be due to limited resources on the host (recommended at least 2CPU and 4GB RAM)\n")
fmt.Printf(" If there is an issue with Mythic server, use 'mythic-cli GetLogs mythic_server' to view potential errors\n")
Status()
fmt.Printf("[*] Fetching GetLogs from mythic_server now:\n")
GetLogs("mythic_server", "500")
os.Exit(1)
}
func TestMythicRabbitmqConnection() {
rabbitmqAddress := "127.0.0.1"
rabbitmqPort := mythicEnv.GetString("RABBITMQ_PORT")
if mythicEnv.GetString("RABBITMQ_HOST") != "mythic_rabbitmq" && mythicEnv.GetString("RABBITMQ_HOST") != "127.0.0.1" {
rabbitmqAddress = mythicEnv.GetString("RABBITMQ_HOST")
}
if rabbitmqAddress == "127.0.0.1" && !isServiceRunning("mythic_rabbitmq") {
log.Fatalf("[-] Service mythic_rabbitmq should be running on the host, but isn't. Containers will be unable to connect.\nStart it by starting Mythic ('sudo ./mythic-cli mythic start') or manually with 'sudo ./mythic-cli mythic start mythic_rabbitmq'\n")
}
maxCount := 10
var err error
count := make([]int, maxCount)
sleepTime := int64(10)
fmt.Printf("[*] Waiting for RabbitMQ to come online (Retry Count = %d)\n", maxCount)
for i := range count {
fmt.Printf("[*] Attempting to connect to RabbitMQ at %s:%s, attempt %d/%d\n", rabbitmqAddress, rabbitmqPort, i+1, maxCount)
conn, err := amqp.Dial(fmt.Sprintf("amqp://%s:%s@%s:%s/mythic_vhost", mythicEnv.GetString("RABBITMQ_USER"), mythicEnv.GetString("RABBITMQ_PASSWORD"), rabbitmqAddress, rabbitmqPort))
if err != nil {
fmt.Printf("[-] Failed to connect to RabbitMQ, retrying in %ds\n", sleepTime)
time.Sleep(10 * time.Second)
} else {
defer conn.Close()
fmt.Printf("[+] Successfully connected to RabbitMQ at amqp://%s:***@%s:%s/mythic_vhost\n\n", mythicEnv.GetString("RABBITMQ_USER"), rabbitmqAddress, rabbitmqPort)
return
}
}
fmt.Printf("[-] Failed to make a connection to the RabbitMQ server: %v\n", err)
if isServiceRunning("mythic_rabbitmq") {
log.Fatalf(" The mythic_rabbitmq service is running, but mythic-cli is unable to connect\n")
} else {
if rabbitmqAddress == "127.0.0.1" {
log.Fatalf(" The mythic_rabbitmq service isn't running, but should be running locally. Did you start it?\n")
} else {
log.Fatalf(" The mythic_rabbitmq service isn't running locally, check to make sure it's running with the proper credentials\n")
}
}
}
func TestPorts() error {
// go through the different services in mythicEnv and check to make sure their ports aren't already used by trying to open them
//MYTHIC_SERVER_HOST:MYTHIC_SERVER_PORT
//POSTGRES_HOST:POSTGRES_PORT
//HASURA_HOST:HASURA_PORT
//RABBITMQ_HOST:RABBITMQ_PORT
//DOCUMENTATION_HOST:DOCUMENTATION_PORT
//NGINX_HOST:NGINX_PORT
portChecks := map[string][]string{
"MYTHIC_SERVER_HOST": {
"MYTHIC_SERVER_PORT",
"mythic_server",
},
"POSTGRES_HOST": {
"POSTGRES_PORT",
"mythic_postgres",
},
"HASURA_HOST": {
"HASURA_PORT",
"mythic_graphql",
},
"RABBITMQ_HOST": {
"RABBITMQ_PORT",
"mythic_rabbitmq",
},
"DOCUMENTATION_HOST": {
"DOCUMENTATION_PORT",
"mythic_documentation",
},
"NGINX_HOST": {
"NGINX_PORT",
"mythic_nginx",
},
"MYTHIC_REACT_HOST": {
"MYTHIC_REACT_PORT",
"mythic_react",
},
"JUPYTER_HOST": {
"JUPYTER_PORT",
"mythic_jupyter",
},
}
var addServices []string
var removeServices []string
if mythicEnv.GetBool("postgres_debug") {
addServices = append(addServices, "mythic_grafana", "mythic_prometheus", "mythic_postgres_exporter")
}
for key, val := range portChecks {
if mythicEnv.GetString(key) == val[1] || mythicEnv.GetString(key) == "127.0.0.1" {
addServices = append(addServices, val[1])
p, err := net.Listen("tcp", ":"+strconv.Itoa(mythicEnv.GetInt(val[0])))
if err != nil {
fmt.Printf("[-] Port %d, from variable %s, appears to already be in use: %v\n", mythicEnv.GetInt(val[0]), key, err)
return err
}
err = p.Close()
if err != nil {
fmt.Printf("[-] Failed to close connection: %v\n", err)
return err
}
} else {
removeServices = append(removeServices, val[1])
}
}
if mythicEnv.GetBool("postgres_debug") {
addServices = append(addServices, "mythic_grafana", "mythic_prometheus", "mythic_postgres_exporter")
}
return nil
}
func PrintMythicConnectionInfo() {
w := new(tabwriter.Writer)
w.Init(os.Stdout, 0, 8, 2, '\t', 0)
fmt.Fprintln(w, "MYTHIC SERVICE\tWEB ADDRESS\tBOUND LOCALLY")
if mythicEnv.GetString("NGINX_HOST") == "mythic_nginx" {
if mythicEnv.GetBool("NGINX_USE_SSL") {
fmt.Fprintln(w, "Nginx (Mythic Web UI)\thttps://127.0.0.1:"+strconv.Itoa(mythicEnv.GetInt("NGINX_PORT"))+"\t", mythicEnv.GetBool("nginx_bind_localhost_only"))
} else {
fmt.Fprintln(w, "Nginx (Mythic Web UI)\thttp://127.0.0.1:"+strconv.Itoa(mythicEnv.GetInt("NGINX_PORT"))+"\t", mythicEnv.GetBool("nginx_bind_localhost_only"))
}
} else {
if mythicEnv.GetBool("NGINX_USE_SSL") {
fmt.Fprintln(w, "Nginx (Mythic Web UI)\thttps://"+mythicEnv.GetString("NGINX_HOST")+":"+strconv.Itoa(mythicEnv.GetInt("NGINX_PORT"))+"\t", mythicEnv.GetBool("nginx_bind_localhost_only"))
} else {
fmt.Fprintln(w, "Nginx (Mythic Web UI)\thttp://"+mythicEnv.GetString("NGINX_HOST")+":"+strconv.Itoa(mythicEnv.GetInt("NGINX_PORT"))+"\t", mythicEnv.GetBool("nginx_bind_localhost_only"))
}
}
if mythicEnv.GetString("MYTHIC_SERVER_HOST") == "mythic_server" {
fmt.Fprintln(w, "Mythic Backend Server\thttp://127.0.0.1:"+strconv.Itoa(mythicEnv.GetInt("MYTHIC_SERVER_PORT"))+"\t", mythicEnv.GetBool("mythic_server_bind_localhost_only"))
} else {
fmt.Fprintln(w, "Mythic Backend Server\thttp://"+mythicEnv.GetString("MYTHIC_SERVER_HOST")+":"+strconv.Itoa(mythicEnv.GetInt("MYTHIC_SERVER_PORT"))+"\t", mythicEnv.GetBool("mythic_server_bind_localhost_only"))
}
if mythicEnv.GetString("HASURA_HOST") == "mythic_graphql" {
fmt.Fprintln(w, "Hasura GraphQL Console\thttp://127.0.0.1:"+strconv.Itoa(mythicEnv.GetInt("HASURA_PORT"))+"\t", mythicEnv.GetBool("hasura_bind_localhost_only"))
} else {
fmt.Fprintln(w, "Hasura GraphQL Console\thttp://"+mythicEnv.GetString("HASURA_HOST")+":"+strconv.Itoa(mythicEnv.GetInt("HASURA_PORT"))+"\t", mythicEnv.GetBool("hasura_bind_localhost_only"))
}
if mythicEnv.GetString("JUPYTER_HOST") == "mythic_jupyter" {
fmt.Fprintln(w, "Jupyter Console\thttp://127.0.0.1:"+strconv.Itoa(mythicEnv.GetInt("JUPYTER_PORT"))+"\t", mythicEnv.GetBool("jupyter_bind_localhost_only"))
} else {
fmt.Fprintln(w, "Jupyter Console\thttp://"+mythicEnv.GetString("JUPYTER_HOST")+":"+strconv.Itoa(mythicEnv.GetInt("JUPYTER_PORT"))+"\t", mythicEnv.GetBool("jupyter_bind_localhost_only"))
}
if mythicEnv.GetString("DOCUMENTATION_HOST") == "mythic_documentation" {
fmt.Fprintln(w, "Internal Documentation\thttp://127.0.0.1:"+strconv.Itoa(mythicEnv.GetInt("DOCUMENTATION_PORT"))+"\t", mythicEnv.GetBool("documentation_bind_localhost_only"))
} else {
fmt.Fprintln(w, "Internal Documentation\thttp://"+mythicEnv.GetString("DOCUMENTATION_HOST")+":"+strconv.Itoa(mythicEnv.GetInt("DOCUMENTATION_PORT"))+"\t", mythicEnv.GetBool("documentation_bind_localhost_only"))
}
fmt.Fprintln(w, "\t\t\t\t")
fmt.Fprintln(w, "ADDITIONAL SERVICES\tIP\tPORT\tBOUND LOCALLY")
if mythicEnv.GetString("POSTGRES_HOST") == "mythic_postgres" {
fmt.Fprintln(w, "Postgres Database\t127.0.0.1\t"+strconv.Itoa(mythicEnv.GetInt("POSTGRES_PORT"))+"\t", mythicEnv.GetBool("postgres_bind_localhost_only"))
} else {
fmt.Fprintln(w, "Postgres Database\t"+mythicEnv.GetString("POSTGRES_HOST")+"\t"+strconv.Itoa(mythicEnv.GetInt("POSTGRES_PORT"))+"\t", mythicEnv.GetBool("postgres_bind_localhost_only"))
}
if mythicEnv.GetString("MYTHIC_REACT_HOST") == "mythic_react" {
fmt.Fprintln(w, "React Server\t127.0.0.1\t"+strconv.Itoa(mythicEnv.GetInt("MYTHIC_REACT_PORT"))+"\t", mythicEnv.GetBool("mythic_react_bind_localhost_only"))
} else {
fmt.Fprintln(w, "React Server\t"+mythicEnv.GetString("MYTHIC_REACT_HOST")+"\t"+strconv.Itoa(mythicEnv.GetInt("MYTHIC_REACT_PORT"))+"\t", mythicEnv.GetBool("mythic_react_bind_localhost_only"))
}
if mythicEnv.GetString("RABBITMQ_HOST") == "mythic_rabbitmq" {
fmt.Fprintln(w, "RabbitMQ\t127.0.0.1\t"+strconv.Itoa(mythicEnv.GetInt("RABBITMQ_PORT"))+"\t", mythicEnv.GetBool("rabbitmq_bind_localhost_only"))
} else {
fmt.Fprintln(w, "RabbitMQ\t"+mythicEnv.GetString("RABBITMQ_HOST")+"\t"+strconv.Itoa(mythicEnv.GetInt("RABBITMQ_PORT"))+"\t", mythicEnv.GetBool("rabbitmq_bind_localhost_only"))
}
fmt.Fprintln(w, "\t\t\t\t")
w.Flush()
}
func Status() {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Fatalf("[-] Failed to get client in Status check: %v", err)
}
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{
All: true,
})
if err != nil {
log.Fatalf("[-] Failed to get container list: %v", err)
}
PrintMythicConnectionInfo()
if len(containers) > 0 {
w := new(tabwriter.Writer)
w.Init(os.Stdout, 0, 8, 2, '\t', 0)
var mythicLocalServices []string
var installedServices []string
sort.Slice(containers[:], func(i, j int) bool {
return containers[i].Labels["name"] < containers[j].Labels["name"]
})
for _, container := range containers {
if container.Labels["name"] == "" {
continue
}
var portRanges []uint16
var portRangeMaps []string
info := fmt.Sprintf("%s\t%s\t%s\t", container.Labels["name"], container.State, container.Status)
if len(container.Ports) > 0 {
sort.Slice(container.Ports[:], func(i, j int) bool {
return container.Ports[i].PublicPort < container.Ports[j].PublicPort
})
for _, port := range container.Ports {
if port.PublicPort > 0 {
if port.PrivatePort == port.PublicPort && port.IP == "0.0.0.0" {
portRanges = append(portRanges, port.PrivatePort)
} else {
portRangeMaps = append(portRangeMaps, fmt.Sprintf("%d/%s -> %s:%d", port.PrivatePort, port.Type, port.IP, port.PublicPort))
}
}
}
if len(portRanges) > 0 {
sort.Slice(portRanges, func(i, j int) bool { return portRanges[i] < portRanges[j] })
}
portString := strings.Join(portRangeMaps[:], ", ")
var stringPortRanges []string
for _, val := range portRanges {
stringPortRanges = append(stringPortRanges, fmt.Sprintf("%d", val))
}
if len(stringPortRanges) > 0 && len(portString) > 0 {
portString = portString + ", "
}
portString = portString + strings.Join(stringPortRanges[:], ", ")
info = info + portString
}
if stringInSlice(container.Image, MythicPossibleServices) {
mythicLocalServices = append(mythicLocalServices, info)
} else {
installedServicesAbsPath, err := filepath.Abs(filepath.Join(getCwdFromExe(), InstalledServicesFolder))
if err != nil {
fmt.Printf("[-] failed to get the absolute path to the Payload_Types folder")
continue
}
for _, mnt := range container.Mounts {
if strings.HasPrefix(mnt.Source, installedServicesAbsPath) {
installedServices = append(installedServices, info)
}
}
}
}
fmt.Fprintln(w, "\t\t\t\t")
fmt.Fprintln(w, "Mythic Main Services")
fmt.Fprintln(w, "CONTAINER NAME\tSTATE\tSTATUS\tPORTS")
for _, line := range mythicLocalServices {
fmt.Fprintln(w, line)
}
fmt.Fprintln(w, "\t\t\t\t")
w.Flush()
fmt.Fprintln(w, "Installed Services")
fmt.Fprintln(w, "CONTAINER NAME\tSTATE\tSTATUS\tPORTS")
for _, line := range installedServices {
fmt.Fprintln(w, line)
}
fmt.Fprintln(w, "\t\t\t\t")
w.Flush()
if len(installedServices) == 0 {
fmt.Printf("[*] There are no services installed\n")
fmt.Printf(" To install one, use \"sudo ./mythic-cli install github <url>\"\n")
fmt.Printf(" Agents can be found at: https://github.com/MythicAgents\n")
fmt.Printf(" C2 Profiles can be found at: https://github.com/MythicC2Profiles\n")
}
} else {
fmt.Println("There are no containers running")
}
if mythicEnv.GetString("RABBITMQ_HOST") == "mythic_rabbitmq" && mythicEnv.GetBool("rabbitmq_bind_localhost_only") {
fmt.Printf("\n[*] RabbitMQ is currently listening on localhost. If you have a remote Service, they will be unable to connect (i.e. one running on another server)")
fmt.Printf("\n Use 'sudo ./mythic-cli config set rabbitmq_bind_localhost_only false' and restart mythic ('sudo ./mythic-cli restart') to change this\n")
}
fmt.Printf("[*] If you are using a remote PayloadType or C2Profile, they will need certain environment variables to properly connect to Mythic.\n")
fmt.Printf(" Use 'sudo ./mythic-cli config service' for easy-to-use configs for these services.\n")
}
func GetLogs(containerName string, numLogs string) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Fatalf("Failed to get client in GetLogs: %v", err)
}
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{})
if err != nil {
log.Fatalf("Failed to get container list: %v", err)
}
if len(containers) > 0 {
found := false
for _, container := range containers {
if container.Labels["name"] == containerName {
found = true
reader, err := cli.ContainerLogs(context.Background(), container.ID, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Tail: numLogs,
})
if err != nil {
log.Fatalf("Failed to get container GetLogs: %v", err)
}
defer reader.Close()
// awesome post about the leading 8 payload/header bytes: https://medium.com/@dhanushgopinath/reading-docker-container-logs-with-golang-docker-engine-api-702233fac044
p := make([]byte, 8)
_, err = reader.Read(p)
for err == nil {
content := make([]byte, binary.BigEndian.Uint32(p[4:]))
reader.Read(content)
fmt.Printf("%s", content)
_, err = reader.Read(p)
}
}
}
if !found {
fmt.Println("[-] Failed to find that container")
}
} else {
fmt.Println("[-] No containers running")
}
}
+245
View File
@@ -0,0 +1,245 @@
package internal
import (
"bufio"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
"log"
"math/big"
"os"
"path"
"path/filepath"
"strings"
"time"
)
func generateRandomPassword(pwLength int) string {
chars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
var b strings.Builder
for i := 0; i < pwLength; i++ {
nBig, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
if err != nil {
log.Fatalf("[-] Failed to generate random number for password generation\n")
}
b.WriteRune(chars[nBig.Int64()])
}
return b.String()
}
func getCwdFromExe() string {
exe, err := os.Executable()
if err != nil {
log.Fatalf("[-] Failed to get path to current executable")
}
return filepath.Dir(exe)
}
func stringInSlice(value string, list []string) bool {
for _, e := range list {
if e == value {
return true
}
}
return false
}
// https://golangcode.com/check-if-a-file-exists/
func fileExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false
}
}
return !info.IsDir()
}
// https://golangcode.com/check-if-a-file-exists/
func dirExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false
}
}
return info.IsDir()
}
// https://blog.depa.do/post/copy-files-and-directories-in-go
func copyFile(src, dst string) error {
var err error
var srcfd *os.File
var dstfd *os.File
var srcinfo os.FileInfo
if srcfd, err = os.Open(src); err != nil {
return err
}
defer srcfd.Close()
if dstfd, err = os.Create(dst); err != nil {
return err
}
defer dstfd.Close()
if _, err = io.Copy(dstfd, srcfd); err != nil {
return err
}
if srcinfo, err = os.Stat(src); err != nil {
return err
}
return os.Chmod(dst, srcinfo.Mode())
}
// https://blog.depa.do/post/copy-files-and-directories-in-go
func copyDir(src string, dst string) error {
var err error
var fds []os.FileInfo
var srcinfo os.FileInfo
if srcinfo, err = os.Stat(src); err != nil {
return err
}
if err = os.MkdirAll(dst, srcinfo.Mode()); err != nil {
return err
}
if fds, err = ioutil.ReadDir(src); err != nil {
return err
}
for _, fd := range fds {
srcfp := path.Join(src, fd.Name())
dstfp := path.Join(dst, fd.Name())
if fd.IsDir() {
if err = copyDir(srcfp, dstfp); err != nil {
fmt.Println(err)
}
} else {
if err = copyFile(srcfp, dstfp); err != nil {
fmt.Println(err)
}
}
}
return nil
}
// https://gist.github.com/r0l1/3dcbb0c8f6cfe9c66ab8008f55f8f28b
func askConfirm(prompt string) bool {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("%s [y/n]: ", prompt)
input, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("[-] Failed to read user input\n")
return false
}
input = strings.ToLower(strings.TrimSpace(input))
if input == "y" || input == "yes" {
return true
} else if input == "n" || input == "no" {
return false
}
}
}
// https://gist.github.com/r0l1/3dcbb0c8f6cfe9c66ab8008f55f8f28b
func askVariable(prompt string) string {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("%s: ", prompt)
input, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("[-] Failed to read user input\n")
return ""
}
input = strings.TrimSpace(input)
return input
}
}
// code to generate self-signed certs pulled from github.com/kabukky/httpscerts
// and from http://golang.org/src/crypto/tls/generate_cert.go.
// only modifications were to use a specific elliptic curve cipher
func checkCerts(certPath string, keyPath string) error {
if _, err := os.Stat(certPath); os.IsNotExist(err) {
return err
} else if _, err := os.Stat(keyPath); os.IsNotExist(err) {
return err
}
return nil
}
func generateCerts() error {
if !dirExists(filepath.Join(getCwdFromExe(), "nginx-docker", "ssl")) {
err := os.MkdirAll(filepath.Join(getCwdFromExe(), "nginx-docker", "ssl"), os.ModePerm)
if err != nil {
fmt.Printf("[-] Failed to make ssl folder in nginx-docker folder\n")
return err
}
fmt.Printf("[+] Successfully made ssl folder in nginx-docker folder\n")
}
certPath := filepath.Join(getCwdFromExe(), "nginx-docker", "ssl", "mythic-cert.crt")
keyPath := filepath.Join(getCwdFromExe(), "nginx-docker", "ssl", "mythic-ssl.key")
if checkCerts(certPath, keyPath) == nil {
return nil
}
fmt.Printf("[*] Failed to find SSL certs for Nginx container, generating now...\n")
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
fmt.Printf("[-] failed to generate private key: %s\n", err)
return err
}
notBefore := time.Now()
oneYear := 365 * 24 * time.Hour
notAfter := notBefore.Add(oneYear)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
fmt.Printf("[-] failed to generate serial number: %s\n", err)
return err
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Mythic"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
fmt.Printf("[-] Failed to create certificate: %s\n", err)
return err
}
certOut, err := os.Create(certPath)
if err != nil {
fmt.Printf("[-] failed to open "+certPath+" for writing: %s\n", err)
return err
}
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
certOut.Close()
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Print("failed to open "+keyPath+" for writing:", err)
return err
}
marshalKey, err := x509.MarshalECPrivateKey(priv)
if err != nil {
fmt.Printf("[-] Unable to marshal ECDSA private key: %v\n", err)
return err
}
pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: marshalKey})
keyOut.Close()
fmt.Printf("[+] Successfully generated new SSL certs\n")
return nil
}
+24
View File
@@ -0,0 +1,24 @@
package cmd
import (
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// configCmd represents the config command
var logsCmd = &cobra.Command{
Use: "logs [container name]",
Short: "Get docker logs from a running service",
Long: `Run this command to get Docker logs from a running service.`,
Run: getLogs,
Args: cobra.ExactArgs(1),
}
func init() {
rootCmd.AddCommand(logsCmd)
logsCmd.Flags().StringP("lines", "l", "500", "Number of lines to display")
}
func getLogs(cmd *cobra.Command, args []string) {
internal.GetLogs(args[0], cmd.Flag("lines").Value.String())
}
+21
View File
@@ -0,0 +1,21 @@
package cmd
import (
"github.com/spf13/cobra"
)
// installCmd represents the config command
var mythicSyncCmd = &cobra.Command{
Use: "mythic_sync",
Short: "work to mythic_sync install/uninstall",
Long: `Run this command's subcommands to install/uninstall mythic_sync `,
Run: mythicSync,
}
func init() {
rootCmd.AddCommand(mythicSyncCmd)
}
func mythicSync(cmd *cobra.Command, args []string) {
}
+22
View File
@@ -0,0 +1,22 @@
package cmd
import (
"github.com/spf13/cobra"
)
// installCmd represents the config command
var installMythicSyncCmd = &cobra.Command{
Use: "install",
Short: "install mythic_sync from GitHub or other Git-based repositories or local folder",
Long: `Run this command to install mythic_sync from Git-based repositories by doing a git clone.
Subcommands of folder/github allow you to specify a local folder or a custom GitHub url + branch to leverage.`,
Run: installMythicSync,
}
func init() {
mythicSyncCmd.AddCommand(installMythicSyncCmd)
}
func installMythicSync(cmd *cobra.Command, args []string) {
}
@@ -0,0 +1,26 @@
package cmd
import (
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// installCmd represents the config command
var installMythicSyncFolderCmd = &cobra.Command{
Use: "folder {path} ",
Short: "install mythic_sync from local folder",
Long: `Run this command to install mythic_sync from a locally cloned folder`,
Run: installMythicSyncFolder,
Args: cobra.ExactArgs(1),
}
func init() {
installMythicSyncCmd.AddCommand(installMythicSyncFolderCmd)
}
func installMythicSyncFolder(cmd *cobra.Command, args []string) {
if err := internal.InstallMythicSyncFolder(args[0]); err != nil {
}
}
@@ -0,0 +1,41 @@
package cmd
import (
"fmt"
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// installCmd represents the config command
var installMythicSyncGitHubCmd = &cobra.Command{
Use: "github [url] [branch] [-f]",
Short: "install services from GitHub or other Git-based repositories",
Long: `Run this command to install services from Git-based repositories by doing a git clone`,
Run: installMythicSyncGitHub,
Args: cobra.RangeArgs(0, 2),
}
func init() {
installMythicSyncCmd.AddCommand(installMythicSyncGitHubCmd)
installMythicSyncGitHubCmd.Flags().BoolVar(
&force,
"f",
false,
`Force installing from GitHub and don't prompt to overwrite files if an older version is already installed'`,
)
}
func installMythicSyncGitHub(cmd *cobra.Command, args []string) {
if args[0] == "" {
if err := internal.InstallMythicSync("https://github.com/GhostManager/mythic_sync", ""); err != nil {
}
} else {
if err := internal.InstallService(args[0], args[1], force); err != nil {
fmt.Printf("[-] Failed to install service: %v\n", err)
} else {
fmt.Printf("[+] Successfully installed service!")
}
}
}
@@ -0,0 +1,22 @@
package cmd
import (
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// installCmd represents the config command
var uninstallMythicSyncCmd = &cobra.Command{
Use: "uninstall",
Short: "uninstall mythic_sync and remove it from disk",
Long: `Run this command to uninstall mythic_sync and remove it from disk`,
Run: uninstallMythicSyncGitHub,
}
func init() {
mythicSyncCmd.AddCommand(uninstallMythicSyncCmd)
}
func uninstallMythicSyncGitHub(cmd *cobra.Command, args []string) {
internal.UninstallMythicSync()
}
+21
View File
@@ -0,0 +1,21 @@
package cmd
import (
"github.com/spf13/cobra"
)
// configCmd represents the config command
var rabbitmqCmd = &cobra.Command{
Use: "rabbitmq",
Short: "interact with the rabbitmq",
Long: `Run this command to interact with rabbitmq`,
Run: rabbitmqCommand,
}
func init() {
rootCmd.AddCommand(rabbitmqCmd)
}
func rabbitmqCommand(cmd *cobra.Command, args []string) {
}
+22
View File
@@ -0,0 +1,22 @@
package cmd
import (
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// configCmd represents the config command
var rabbitmqResetCmd = &cobra.Command{
Use: "reset",
Short: "reset rabbitmq",
Long: `Run this command to stop mythic and delete the current RabbitMQ storage`,
Run: rabbitmqReset,
}
func init() {
rabbitmqCmd.AddCommand(rabbitmqResetCmd)
}
func rabbitmqReset(cmd *cobra.Command, args []string) {
internal.RabbitmqReset(true)
}
+25
View File
@@ -0,0 +1,25 @@
package cmd
import (
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// configCmd represents the config command
var removeDockerComposeCmd = &cobra.Command{
Use: "remove [service name]",
Short: "Remove local service folder from docker compose",
Long: `Run this command to remove a local Mythic service folder from docker-compose.`,
Run: removeDockerCompose,
Args: cobra.ExactArgs(1),
}
func init() {
rootCmd.AddCommand(removeDockerComposeCmd)
}
func removeDockerCompose(cmd *cobra.Command, args []string) {
if err := internal.RemoveDockerComposeEntry(args[0]); err != nil {
}
}
+18
View File
@@ -0,0 +1,18 @@
package cmd
import (
"github.com/spf13/cobra"
)
// configCmd represents the config command
var restartCmd = &cobra.Command{
Use: "restart",
Short: "Start all of Mythic",
Long: `Run this command restart all Mythic containers. Use subcommands to
adjust specific containers to restart.`,
Run: start,
}
func init() {
rootCmd.AddCommand(restartCmd)
}
+32
View File
@@ -0,0 +1,32 @@
package cmd
import (
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"os"
)
import (
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "mythic-cli",
Short: "A command line interface for managing Mythic.",
Long: `Mythic CLI is a command line interface for managing the Mythic
application and associated containers and services. Commands are grouped by their use.`,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func init() {
// Create or parse the Docker ``.env`` file
internal.Initialize()
}
+24
View File
@@ -0,0 +1,24 @@
package cmd
import (
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// configCmd represents the config command
var startCmd = &cobra.Command{
Use: "start [container names]",
Short: "Start Mythic containers",
Long: `Run this command to start all Mythic containers. If you want to only start certain containers, specify their names.`,
Run: start,
}
func init() {
rootCmd.AddCommand(startCmd)
}
func start(cmd *cobra.Command, args []string) {
if err := internal.DockerStart(args); err != nil {
}
}
+22
View File
@@ -0,0 +1,22 @@
package cmd
import (
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// configCmd represents the config command
var statusCmd = &cobra.Command{
Use: "status",
Short: "Get current Mythic container status",
Long: `Run this command to get the current status of the Mythic services and containers.`,
Run: status,
}
func init() {
rootCmd.AddCommand(statusCmd)
}
func status(cmd *cobra.Command, args []string) {
internal.Status()
}
+25
View File
@@ -0,0 +1,25 @@
package cmd
import (
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// configCmd represents the config command
var stopCmd = &cobra.Command{
Use: "stop",
Short: "Stop all of Mythic",
Long: `Run this command stop all Mythic containers. Use subcommands to
adjust specific containers to stop.`,
Run: stop,
}
func init() {
rootCmd.AddCommand(stopCmd)
}
func stop(cmd *cobra.Command, args []string) {
if err := internal.DockerStop(args); err != nil {
}
}
+23
View File
@@ -0,0 +1,23 @@
package cmd
import (
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// configCmd represents the config command
var testCmd = &cobra.Command{
Use: "test",
Short: "test mythic service connections",
Long: `Run this command to test mythic connections to RabbitMQ and the Mythic UI`,
Run: test,
}
func init() {
rootCmd.AddCommand(testCmd)
}
func test(cmd *cobra.Command, args []string) {
internal.TestMythicRabbitmqConnection()
internal.TestMythicConnection()
}
+22
View File
@@ -0,0 +1,22 @@
package cmd
import (
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
"github.com/spf13/cobra"
)
// installCmd represents the config command
var uninstallCmd = &cobra.Command{
Use: "uninstall [container name]",
Short: "uninstall services locally and remove them from disk",
Long: `Run this command to uninstall a local Mythic service and remove its contents from disk`,
Run: uninstall,
}
func init() {
rootCmd.AddCommand(uninstallCmd)
}
func uninstall(cmd *cobra.Command, args []string) {
internal.UninstallService(args)
}
+46
View File
@@ -0,0 +1,46 @@
module github.com/MythicMeta/Mythic_CLI
go 1.18
require (
github.com/docker/docker v20.10.6+incompatible
github.com/spf13/cobra v1.0.0
github.com/spf13/viper v1.12.0
github.com/streadway/amqp v1.0.0
)
require (
github.com/Microsoft/go-winio v0.4.17 // indirect
github.com/containerd/containerd v1.5.0 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.7.0 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.3.0 // indirect
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect
google.golang.org/grpc v1.46.2 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect
)
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
// @author: its_a_feature_
// This code is to administer Mythic 3.0.0+ configurations
package main
import (
"github.com/MythicMeta/Mythic_CLI/cmd"
)
func main() {
cmd.Execute()
}
+10 -10
View File
@@ -1,5 +1,9 @@
# Mythic
A cross-platform, post-exploit, red teaming framework built with python3, docker, docker-compose, and a web browser UI. It's designed to provide a collaborative and user friendly interface for operators, managers, and reporting throughout red teaming.
A cross-platform, post-exploit, red teaming framework built with GoLang, docker, docker-compose, and a web browser UI. It's designed to provide a collaborative and user friendly interface for operators, managers, and reporting throughout red teaming.
## Starting Mythic
Mythic is controlled via the `mythic-cli` binary. To generate the binary, run `sudo make`. From there, you can run `sudo ./mythic-cli start` to bring up all of the default Mythic containers.
## Details
* Check out a [series of YouTube videos](https://www.youtube.com/playlist?list=PLHVFedjbv6sNLB1QqnGJxRBMukPRGYa-H) showing how Mythic looks/works and highlighting a few key features
@@ -15,7 +19,7 @@ A cross-platform, post-exploit, red teaming framework built with python3, docker
The Mythic repository itself does not host any Payload Types or any C2 Profiles. Instead, Mythic provides a command, `./mythic-cli install github <url> [branch name] [-f]`, that can be used to install agents into a current Mythic instance.
Payload Types are hosted on the [MythicAgents](https://github.com/MythicAgents) organization and C2 Profiles are hosted on the [MythicC2Profiles](https://github.com/MythicC2Profiles) organization.
Payload Types are hosted on the [MythicAgents](https://github.com/MythicAgents) organization and C2 Profiles are hosted on the [MythiC2Profiles](https://github.com/MythicC2Profiles) organization.
To install an agent, simply run the script and provide an argument of the path to the agent on GitHub:
```bash
@@ -33,23 +37,19 @@ This is a slight departure from previous Mythic versions which included a few de
Mythic uses Docker and Docker-compose for all of its components, which allows Mythic to provide a wide range of components and features without having requirements exist on the host. However, it can be helpful to have insight into how the containers are configured. All of Mythic's docker containers are hosted on DockerHub under [itsafeaturemythic](https://hub.docker.com/search?q=itsafeaturemythic&type=image).
Additionally, Mythic uses a number of custom PyPi packages to help control and sync information between all of the containers as well as providing an easy way to script access to the server.
Additionally, Mythic uses a custom PyPi package (mythic_container) and a custom Golang package (https://github.com/MythicMeta/MythicContainer) to help control and sync information between all of the containers as well as providing an easy way to script access to the server.
All of this can be found on the [MythicMeta](https://github.com/MythicMeta):
* Dockerfile configurations for all Docker images uploaded to DockerHub
* PyPi source code for all packages uploaded to PyPi
* Scripting source code
## Current Container PyPi Package requirements
Supported payload types must have the `mythic_payloadtype_container` PyPi package of 0.0.43.
* The Payload Type container reports this as version 7.
The current PyPi package for Mythic v3.0.0 is mythic_container==0.2.0-rc1.
Supported c2 profiles must have the `mythic_c2_container` PyPi package of 0.0.22.
* The C2 Profile container reports this as version 3.
## Current Container Golang Package requirements
Supported translation containers must have the `mythic_translator_containter` PyPi package of 0.0.10.
* The Translator container reports this as version 3.
The current Golang package for Mythic v3.0.0 is at github.com/MythicMeta/MythicContainer
## Documentation
+1 -1
View File
@@ -1 +1 @@
2.3.13
3.0.0
-186
View File
@@ -1,186 +0,0 @@
services:
mythic_documentation:
build: ./documentation-docker
command: server
container_name: mythic_documentation
image: mythic_documentation
labels:
name: mythic_documentation
logging:
driver: json-file
options:
max-file: "1"
max-size: 10m
ports:
- ${DOCUMENTATION_PORT}:1313
restart: always
volumes:
- ./documentation-docker/:/src
mythic_graphql:
build: ./hasura-docker
container_name: mythic_graphql
depends_on:
- mythic_postgres
environment:
- HASURA_GRAPHQL_DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
- HASURA_GRAPHQL_METADATA_DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
- HASURA_GRAPHQL_ENABLE_CONSOLE=true
- HASURA_GRAPHQL_DEV_MODE=true
- HASURA_GRAPHQL_ADMIN_SECRET=${HASURA_SECRET}
- HASURA_GRAPHQL_INSECURE_SKIP_TLS_VERIFY=true
- HASURA_GRAPHQL_SERVER_PORT=${HASURA_PORT}
- HASURA_GRAPHQL_METADATA_DIR=/metadata
- HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL=500
- HASURA_GRAPHQL_AUTH_HOOK=http://${MYTHIC_SERVER_HOST}:${MYTHIC_SERVER_PORT}/graphql/webhook
- MYTHIC_ACTIONS_URL_BASE=http://${MYTHIC_SERVER_HOST}:${MYTHIC_SERVER_PORT}/api/v1.4
image: mythic_graphql
labels:
name: mythic_graphql
logging:
driver: json-file
options:
max-file: "1"
max-size: 10m
network_mode: host
restart: always
volumes:
- ./hasura-docker/metadata:/metadata
mythic_nginx:
build: ./nginx-docker
container_name: mythic_nginx
environment:
- DOCUMENTATION_HOST=${DOCUMENTATION_HOST}
- DOCUMENTATION_PORT=${DOCUMENTATION_PORT}
- NGINX_PORT=${NGINX_PORT}
- MYTHIC_SERVER_HOST=${MYTHIC_SERVER_HOST}
- MYTHIC_SERVER_PORT=${MYTHIC_SERVER_PORT}
- HASURA_HOST=${HASURA_HOST}
- HASURA_PORT=${HASURA_PORT}
- NEW_UI_HOST=${MYTHIC_SERVER_HOST}
- NEW_UI_PORT=3000
image: mythic_nginx
labels:
name: mythic_nginx
logging:
driver: json-file
options:
max-file: "1"
max-size: 10m
network_mode: host
restart: always
volumes:
- ./nginx-docker/ssl:/etc/ssl/private
- ./nginx-docker/config:/etc/nginx
mythic_postgres:
build: ./postgres-docker
command: postgres -c "max_connections=400"
container_name: mythic_postgres
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
image: mythic_postgres
labels:
name: mythic_postgres
logging:
driver: json-file
options:
max-file: "1"
max-size: 10m
ports:
- ${POSTGRES_PORT}:5432
restart: always
volumes:
- ./postgres-docker/database:/var/lib/postgresql/data
mythic_rabbitmq:
build: ./rabbitmq-docker
container_name: mythic_rabbitmq
environment:
- RABBITMQ_DEFAULT_USER=${RABBITMQ_USER}
- RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD}
- RABBITMQ_DEFAULT_VHOST=${RABBITMQ_VHOST}
image: mythic_rabbitmq
labels:
name: mythic_rabbitmq
logging:
driver: json-file
options:
max-file: "1"
max-size: 10m
ports:
- ${RABBITMQ_PORT}:5672
restart: always
volumes:
- ./rabbitmq-docker/storage:/var/lib/rabbitmq
mythic_react:
build: ./mythic-react-docker
container_name: mythic_react
image: mythic_react
labels:
name: mythic_react
logging:
driver: json-file
options:
max-file: "1"
max-size: 10m
network_mode: host
restart: always
volumes:
- ./mythic-react-docker/mythic:/mythic_react
mythic_redis:
build: ./redis-docker
container_name: mythic_redis
image: mythic_redis
labels:
name: mythic_redis
logging:
driver: json-file
options:
max-file: "1"
max-size: 10m
ports:
- ${REDIS_PORT}:6379
restart: always
mythic_server:
build: ./mythic-docker
command: /bin/bash /Mythic/start_mythic_server.sh
container_name: mythic_server
depends_on:
- mythic_postgres
environment:
- MYTHIC_POSTGRES_HOST=${POSTGRES_HOST}
- MYTHIC_POSTGRES_PORT=${POSTGRES_PORT}
- MYTHIC_POSTGRES_DB=${POSTGRES_DB}
- MYTHIC_POSTGRES_USER=${POSTGRES_USER}
- MYTHIC_POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- MYTHIC_RABBITMQ_HOST=${RABBITMQ_HOST}
- MYTHIC_RABBITMQ_PORT=${RABBITMQ_PORT}
- MYTHIC_RABBITMQ_USER=${RABBITMQ_USER}
- MYTHIC_RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD}
- MYTHIC_RABBITMQ_VHOST=${RABBITMQ_VHOST}
- MYTHIC_JWT_SECRET=${JWT_SECRET}
- MYTHIC_REDIS_PORT=${REDIS_PORT}
- MYTHIC_DEBUG
- MYTHIC_ADMIN_PASSWORD
- MYTHIC_ADMIN_USER
- MYTHIC_SERVER_PORT
- MYTHIC_ALLOWED_IP_BLOCKS=${ALLOWED_IP_BLOCKS}
- MYTHIC_DEFAULT_OPERATION_NAME=${DEFAULT_OPERATION_NAME}
- MYTHIC_NGINX_PORT=${NGINX_PORT}
- MYTHIC_SERVER_HEADER=${SERVER_HEADER}
- MYTHIC_WEB_LOG_SIZE=${WEB_LOG_SIZE}
- MYTHIC_WEB_KEEP_LOGS=${WEB_KEEP_LOGS}
- MYTHIC_SIEM_LOG_NAME=${SIEM_LOG_NAME}
image: mythic_server
labels:
name: mythic_server
logging:
driver: json-file
options:
max-file: "1"
max-size: 10m
network_mode: host
restart: always
volumes:
- ./mythic-docker:/Mythic
version: "2.4"
+61
View File
@@ -0,0 +1,61 @@
FROM golang:1.20-buster
ARG CA_CERTIFICATE
ARG NPM_REGISTRY
ARG PYPI_INDEX
ARG PYPI_INDEX_URL
ARG DOCKER_REGISTRY_MIRROR
ARG HTTP_PROXY
ARG HTTPS_PROXY
# install .net core
RUN wget https://packages.microsoft.com/config/debian/10/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
RUN dpkg -i packages-microsoft-prod.deb
RUN rm packages-microsoft-prod.deb
# install mono, protoc protobufs, and .net core
RUN apt-get -y update && \
apt-get -y autoremove && \
apt-get clean && \
apt install --no-install-recommends -y apt-transport-https dirmngr gnupg ca-certificates && \
apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
echo "deb https://download.mono-project.com/repo/debian stable-buster main" | tee /etc/apt/sources.list.d/mono-official-stable.list && \
apt update && apt-get install --no-install-recommends software-properties-common apt-utils make build-essential libssl-dev zlib1g-dev libbz2-dev \
libreadline-dev libsqlite3-dev llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev mono-complete protobuf-compiler dotnet-sdk-7.0 gcc-mingw-w64 -y
# install python 3.11
RUN wget https://www.python.org/ftp/python/3.11.1/Python-3.11.1.tgz
RUN tar xvf Python-3.11.1.tgz
RUN Python-3.11.1/configure --with-ensurepip=install
RUN make -j8
RUN make install
COPY requirements.txt /
RUN pip3 install -r /requirements.txt
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
RUN mkdir -p "/Windows/assembly/GAC_MSIL/System.Management.Automation/1.0.0.0__31bf3856ad364e35"
COPY ["System.Management.Automation.dll", "/Windows/assembly/GAC_MSIL/System.Management.Automation/1.0.0.0__31bf3856ad364e35/System.Management.Automation.dll"]
RUN mkdir -p "/Windows/Microsoft.NET/assembly/GAC_MSIL/System.Management.Automation/v4.0_3.0.0.0__31bf3856ad364e35"
COPY ["System.Management.Automation.dll-v4", "/Windows/Microsoft.NET/assembly/GAC_MSIL/System.Management.Automation/v4.0_3.0.0.0__31bf3856ad364e35/System.Management.Automation.dll"]
# allow cross compilation for macOS
# Configure the container for OSX cross compilation
RUN ln -s /usr/include/asm-generic /usr/include/asm
RUN bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"
RUN apt-get install clang-15 clang-15++ cmake -y
RUN ln -s /usr/bin/clang-15 /usr/bin/clang
RUN ln -s /usr/bin/clang++-15 /usr/bin/clang++
ENV OSX_SDK 12.1
RUN cd / && git clone --depth 1 https://github.com/tpoechtrager/osxcross.git && \
cd osxcross && curl -fsSL -O https://github.com/joseluisq/macosx-sdks/releases/download/${OSX_SDK}/MacOSX${OSX_SDK}.sdk.tar.xz && \
mv MacOSX${OSX_SDK}.sdk.tar.xz tarballs/ && UNATTENDED=yes ./build.sh && rm -rf /build/osxcross/tarballs/
ENV PATH /osxcross/target/bin:$PATH
ENV LD_LIBRARY_PATH=/osxcross/target/lib
# install garble support
RUN go install mvdan.cc/garble@latest
Binary file not shown.
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
aio-pika==9.0.4
dynaconf==3.1.11
ujson==5.7.0
aiohttp==3.8.3
psutil==5.9.4
grpcio
grpcio-tools
pycryptodome
+1 -1
View File
@@ -1,3 +1,3 @@
From klakegg/hugo
FROM klakegg/hugo:latest
RUN hugo new site mythic_docs

Some files were not shown because too many files have changed in this diff Show More