mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
initial commit of the new Mythic 3.0.0 server
This commit is contained in:
@@ -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 +0,0 @@
|
||||
FROM itsafeaturemythic/python38_sanic_c2profile:0.0.6
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Page Not Found!</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
@@ -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 +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 +0,0 @@
|
||||
FROM itsafeaturemythic/python38_translator_container:0.0.5
|
||||
@@ -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}
|
||||
@@ -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/"
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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 .
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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, "")
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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, "")
|
||||
}
|
||||
@@ -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.")
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
@@ -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!")
|
||||
}
|
||||
}
|
||||
@@ -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!")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
@@ -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,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"
|
||||
@@ -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.
@@ -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,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
Reference in New Issue
Block a user