Skip to content

Commit

Permalink
Merge pull request #236 from ulricvbs/master
Browse files Browse the repository at this point in the history
Update Example_C2_Profile Base Image
  • Loading branch information
its-a-feature authored Jun 23, 2022
2 parents ad8e566 + 7cf0244 commit 50ba9f2
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 24 deletions.
2 changes: 1 addition & 1 deletion Example_C2_Profile/Dockerfile
Original file line number Diff line number Diff line change
@@ -1 +1 @@
FROM itsafeaturemythic/python38_sanic_c2profile:0.0.4
FROM itsafeaturemythic/python38_sanic_c2profile:0.0.6
3 changes: 2 additions & 1 deletion Example_C2_Profile/c2_code/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"port": 80,
"key_path": "",
"cert_path": "",
"use_ssl": false,
"debug": false
}
]
}
}
36 changes: 34 additions & 2 deletions Example_C2_Profile/c2_code/server
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
Expand All @@ -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']:
Expand All @@ -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)
Expand Down
217 changes: 198 additions & 19 deletions Example_C2_Profile/mythic/c2_functions/C2_RPC_functions.py
Original file line number Diff line number Diff line change
@@ -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"}
"""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}
8 changes: 7 additions & 1 deletion Example_C2_Profile/mythic/c2_functions/HTTP.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down

0 comments on commit 50ba9f2

Please sign in to comment.