diff --git a/Example_C2_Profile/Dockerfile b/Example_C2_Profile/Dockerfile index 38cbe3983..52403b863 100755 --- a/Example_C2_Profile/Dockerfile +++ b/Example_C2_Profile/Dockerfile @@ -1 +1 @@ -FROM itsafeaturemythic/python38_sanic_c2profile:0.0.4 +FROM itsafeaturemythic/python38_sanic_c2profile:0.0.6 diff --git a/Example_C2_Profile/c2_code/config.json b/Example_C2_Profile/c2_code/config.json index 1060cfdfe..186f094aa 100755 --- a/Example_C2_Profile/c2_code/config.json +++ b/Example_C2_Profile/c2_code/config.json @@ -11,7 +11,8 @@ "port": 80, "key_path": "", "cert_path": "", + "use_ssl": false, "debug": false } ] -} \ No newline at end of file +} diff --git a/Example_C2_Profile/c2_code/server b/Example_C2_Profile/c2_code/server index 913b89d4c..a3c74c652 100755 --- a/Example_C2_Profile/c2_code/server +++ b/Example_C2_Profile/c2_code/server @@ -1,5 +1,10 @@ #!/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 @@ -14,11 +19,28 @@ 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() @@ -27,6 +49,14 @@ def server_error_handler(request, exception): 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']: @@ -35,11 +65,13 @@ async def agent_message(request, **kwargs): 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 + # 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 + # 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) diff --git a/Example_C2_Profile/mythic/c2_functions/C2_RPC_functions.py b/Example_C2_Profile/mythic/c2_functions/C2_RPC_functions.py index 1a4aa6b24..1a33b7558 100644 --- a/Example_C2_Profile/mythic/c2_functions/C2_RPC_functions.py +++ b/Example_C2_Profile/mythic/c2_functions/C2_RPC_functions.py @@ -1,27 +1,206 @@ -from mythic_c2_container.C2ProfileBase import * -import sys +"""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 -# request is a dictionary: {"action": func_name, "message": "the input", "task_id": task id num} -# must return an RPCResponse() object and set .status to an instance of RPCStatus and response to str of message 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 MythicCallbackRPC.MythicCallbackRPC().add_event_message(message="got a POST message") + resp = await MythicRPC().execute("create_event_message", message="Test message", warning=False) return response - -# The opsec function is called when a payload is created as a check to see if the parameters supplied are good -# The input for "request" is a dictionary of: -# { -# "action": "opsec", -# "parameters": { -# "param_name": "param_value", -# "param_name2: "param_value2", -# } -# } -# This function should return one of two things: -# For success: {"status": "success", "message": "your success message here" } -# For error: {"status": "error", "error": "your error message here" } async def opsec(request): - return {"status": "success", "message": "No OPSEC Check Performed"} \ No newline at end of file + """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": "" } + error: {"status": "error", "error": "" } + """ + # 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": "" } + error: {"status": "error", "error": "" } + """ + # 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": "" } + error: {"status": "error", "error": "" } + + """ + # 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} diff --git a/Example_C2_Profile/mythic/c2_functions/HTTP.py b/Example_C2_Profile/mythic/c2_functions/HTTP.py index 38816cf96..6fc481bf5 100644 --- a/Example_C2_Profile/mythic/c2_functions/HTTP.py +++ b/Example_C2_Profile/mythic/c2_functions/HTTP.py @@ -1,5 +1,11 @@ -from mythic_c2_container.C2ProfileBase import * +"""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"