mirror of
https://github.com/MythicC2Profiles/http
synced 2026-06-08 11:56:36 +00:00
updating http profile to support Mythic version 2.2.12
This commit is contained in:
@@ -1 +1 @@
|
||||
FROM itsafeaturemythic/python38_sanic_c2profile:0.0.5
|
||||
FROM itsafeaturemythic/python38_sanic_c2profile:0.0.6
|
||||
@@ -9,9 +9,10 @@
|
||||
"Content-Type": "application/javascript; charset=utf-8"
|
||||
},
|
||||
"port": 80,
|
||||
"key_path": "",
|
||||
"cert_path": "",
|
||||
"debug": false
|
||||
"key_path": "privkey.pem",
|
||||
"cert_path": "fullchain.pem",
|
||||
"debug": false,
|
||||
"use_ssl": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import requests
|
||||
import json
|
||||
import os
|
||||
import aiohttp
|
||||
from OpenSSL import crypto, SSL
|
||||
|
||||
config = {}
|
||||
|
||||
@@ -45,6 +46,11 @@ async def agent_message(request, **kwargs):
|
||||
await print_flush("Forwarding along to: {}".format(config['mythic_address']))
|
||||
if request.method == "POST":
|
||||
# manipulate the request if needed
|
||||
#from mythic_c2_container.MythicRPC import MythicRPC
|
||||
#msg = await MythicRPC().execute("create_encrypted_message", message={"action": "get_tasking"}, target_uuid="ba3f9b0a-e073-4cbc-afe7-1c94f86cc76a", c2_profile_name="http")
|
||||
#resp = await MythicRPC().execute("create_event_message", message=msg.response, warning=False)
|
||||
#msg = await MythicRPC().execute("create_decrypted_message", message=msg.response, c2_profile_name="http")
|
||||
#resp = await MythicRPC().execute("create_event_message", message=msg.response, warning=False)
|
||||
async with session.post(config['mythic_address'], data=request.body, ssl=False, headers={"Mythic": "http", **request.headers, **forwarded_headers}) as resp:
|
||||
return raw(await resp.read(), status=resp.status, headers=config[request.app.name]['headers'])
|
||||
else:
|
||||
@@ -59,12 +65,50 @@ async def agent_message(request, **kwargs):
|
||||
await print_flush("error in agent_message: {}".format(str(e)))
|
||||
return server_error_handler(request, e)
|
||||
|
||||
# from https://stackoverflow.com/questions/27164354/create-a-self-signed-x509-certificate-in-python
|
||||
def cert_gen(
|
||||
emailAddress: str = "",
|
||||
commonName: str = "",
|
||||
countryName: str = "US",
|
||||
localityName: str = "",
|
||||
stateOrProvinceName: str = "",
|
||||
organizationName: str = "",
|
||||
organizationUnitName: str = "",
|
||||
serialNumber: int = 0,
|
||||
validityStartInSeconds: int = 0,
|
||||
validityEndInSeconds: int = 10*365*24*60*60,
|
||||
key_file: str = "privkey.pem",
|
||||
cert_file: str = "fullchain.pem"):
|
||||
|
||||
#can look at generated file using openssl:
|
||||
#openssl x509 -inform pem -in selfsigned.crt -noout -text
|
||||
# create a key pair
|
||||
k = crypto.PKey()
|
||||
k.generate_key(crypto.TYPE_RSA, 4096)
|
||||
# create a self-signed cert
|
||||
cert = crypto.X509()
|
||||
cert.get_subject().C = countryName
|
||||
cert.get_subject().ST = stateOrProvinceName
|
||||
cert.get_subject().L = localityName
|
||||
cert.get_subject().O = organizationName
|
||||
cert.get_subject().OU = organizationUnitName
|
||||
cert.get_subject().CN = commonName
|
||||
cert.get_subject().emailAddress = emailAddress
|
||||
cert.set_serial_number(serialNumber)
|
||||
cert.gmtime_adj_notBefore(0)
|
||||
cert.gmtime_adj_notAfter(validityEndInSeconds)
|
||||
cert.set_issuer(cert.get_subject())
|
||||
cert.set_pubkey(k)
|
||||
cert.sign(k, 'sha512')
|
||||
with open(cert_file, "wt") as f:
|
||||
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("utf-8"))
|
||||
with open(key_file, "wt") as f:
|
||||
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode("utf-8"))
|
||||
|
||||
|
||||
|
||||
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...")
|
||||
@@ -95,7 +139,10 @@ if __name__ == "__main__":
|
||||
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():
|
||||
if (not keyfile.is_file() or not certfile.is_file()) and inst["use_ssl"]:
|
||||
# we need to generate the certs
|
||||
cert_gen(key_file=inst['key_path'], cert_file=inst['cert_path'])
|
||||
if keyfile.is_file() and certfile.is_file() and inst['use_ssl']:
|
||||
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain(inst['cert_path'], keyfile=inst['key_path'])
|
||||
if inst['debug']:
|
||||
@@ -109,20 +156,21 @@ if __name__ == "__main__":
|
||||
print("++++++++++++++++++++++++++++++++++++++++++")
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
if inst["key_path"] != "" or inst["cert_path"] != "":
|
||||
if (inst["key_path"] != "" or inst["cert_path"] != "") and inst["use_ssl"]:
|
||||
print("-----------------------------------------------------------------------------")
|
||||
print("-----------------------------------------------------------------------------")
|
||||
print("key_path or cert_path is not empty, but at least one of the files cannot be found.")
|
||||
print("key_path or cert_path is not empty and use_ssl is true, but at least one of the files cannot be found.")
|
||||
print("If you're using the http profile in a Docker container, place your cert and key in Mythic/C2_Profiles/http/c2_code/")
|
||||
print(" and set the key_path and cert_path values to just the names of the files")
|
||||
print("If you're using the http profile outside of Docker, make sure the paths are absolute and correct")
|
||||
print("NOT using SSL for port {}".format(inst['port']))
|
||||
print("-----------------------------------------------------------------------------")
|
||||
print("-----------------------------------------------------------------------------")
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
print("**************************************************************")
|
||||
print("**************************************************************")
|
||||
print("No Cert or Key file specified, NOT using SSL for port {}".format(inst['port']))
|
||||
print("NOT using SSL for port {}".format(inst['port']))
|
||||
print("**************************************************************")
|
||||
print("**************************************************************")
|
||||
sys.stdout.flush()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from mythic_c2_container.C2ProfileBase import *
|
||||
from mythic_c2_container.MythicRPC import *
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
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
|
||||
@@ -7,7 +10,7 @@ async def test(request):
|
||||
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
|
||||
|
||||
|
||||
@@ -24,4 +27,148 @@ async def test(request):
|
||||
# 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"}
|
||||
if request["parameters"]["callback_host"] == "https://domain.com":
|
||||
return {"status": "error", "error": "Callback Host is set to default of https://domain.com!\n"}
|
||||
if request["parameters"]["callback_host"].count(":") == 2:
|
||||
return {"status": "error", "error": f"Callback Host specifies a port ({request['parameters']['callback_host']})! This should be omitted and specified in the Callback Port parameter.\n"}
|
||||
if "https" in request["parameters"]["callback_host"] and request["parameters"]["callback_port"] not in ["443", "8443", "7443"]:
|
||||
return {"status": "success", "message": f"Mismatch in callback host: HTTPS specified, but port {request['parameters']['callback_port']}, is not standard HTTPS port\n"}
|
||||
return {"status": "success", "message": "Basic OPSEC Check Passed\n"}
|
||||
|
||||
|
||||
# The config_check function is called when a payload is created as a check to see if the parameters supplied
|
||||
# to the agent match up with what's in the C2 profile
|
||||
# The input for "request" is a dictionary of:
|
||||
# {
|
||||
# "action": "config_check",
|
||||
# "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 config_check(request):
|
||||
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)}
|
||||
|
||||
|
||||
# The redirect_rules function is called on demand by an operator to generate redirection rules for a specific payload
|
||||
# The input for "request" is a dictionary of:
|
||||
# {
|
||||
# "action": "redirect_rules",
|
||||
# "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 redirect_rules(request):
|
||||
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,18 +8,18 @@ This is a Mythic C2 Profile called http. It simply provides a way to get HTTP me
|
||||
* Proxy Information
|
||||
* Support for SSL
|
||||
|
||||
The c2 profile has `mythic_c2_container==0.0.22` PyPi package installed and reports to Mythic as version "3".
|
||||
The c2 profile has `mythic_c2_container==0.0.23` PyPi package installed and reports to Mythic as version "4".
|
||||
|
||||
## How to install an agent in this format within Mythic
|
||||
|
||||
When it's time for you to test out your install or for another user to install your agent, it's pretty simple. Within Mythic is a `install_agent_from_github.sh` script (https://github.com/its-a-feature/Mythic/blob/master/install_agent_from_github.sh). You can run this in one of two ways:
|
||||
When it's time for you to test out your install or for another user to install your c2 profile, it's pretty simple. Within Mythic you can run the `mythic-cli` binary to install this in one of three ways:
|
||||
|
||||
* `sudo ./install_agent_from_github.sh https://github.com/user/repo` to install the main branch
|
||||
* `sudo ./install_agent_from_github.sh https://github.com/user/repo branchname` to install a specific branch of that repo
|
||||
* `sudo ./mythic-cli install github https://github.com/user/repo` to install the main branch
|
||||
* `sudo ./mythic-cli install github https://github.com/user/repo branchname` to install a specific branch of that repo
|
||||
* `sudo ./mythic-cli install folder /path/to/local/folder/cloned/from/github` to install from an already cloned down version of an agent repo
|
||||
|
||||
Now, you might be wondering _when_ should you or a user do this to properly add your agent to their Mythic instance. There's no wrong answer here, just depends on your preference. The three options are:
|
||||
|
||||
* Mythic is already up and going, then you can run the install script and just direct that agent's containers to start (i.e. `sudo ./start_payload_types.sh agentName` and if that agent has its own special C2 containers, you'll need to start them too via `sudo ./start_c2_profiles.sh c2profileName`).
|
||||
* Mythic is already up and going, but you want to minimize your steps, you can just install the agent and run `sudo ./start_mythic.sh`. That script will first _stop_ all of your containers, then start everything back up again. This will also bring in the new agent you just installed.
|
||||
* Mythic isn't running, you can install the script and just run `sudo ./start_mythic.sh`.
|
||||
Now, you might be wondering _when_ should you or a user do this to properly add your profile to their Mythic instance. There's no wrong answer here, just depends on your preference. The three options are:
|
||||
|
||||
* Mythic is already up and going, then you can run the install script and just direct that profile's containers to start (i.e. `sudo ./mythic-cli c2 start profileName`.
|
||||
* Mythic is already up and going, but you want to minimize your steps, you can just install the profile and run `sudo ./mythic-cli mythic start`. That script will first _stop_ all of your containers, then start everything back up again. This will also bring in the new profile you just installed.
|
||||
* Mythic isn't running, you can install the script and just run `sudo ./mythic-cli mythic start`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
+++
|
||||
title = "HTTP"
|
||||
title = "http"
|
||||
chapter = false
|
||||
weight = 5
|
||||
+++
|
||||
@@ -39,9 +39,10 @@ The profile reads a `config.json` file for a set of instances of `Sanic` webserv
|
||||
"Content-Type": "application/javascript; charset=utf-8"
|
||||
},
|
||||
"port": 80,
|
||||
"key_path": "",
|
||||
"cert_path": "",
|
||||
"debug": true
|
||||
"key_path": "privkey.pem",
|
||||
"cert_path": "fullchain.pem",
|
||||
"debug": true,
|
||||
"use_ssl": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -49,7 +50,7 @@ The profile reads a `config.json` file for a set of instances of `Sanic` webserv
|
||||
|
||||
You can specify the headers that the profile will set on Server responses. If there's an error, the server will return a `404` message based on the `fake.html` file contents in `C2_Profiles/HTTP/c2_code`.
|
||||
|
||||
If you want to use SSL within this container specifically, then you can put your key and cert in the `C2_Profiles/HTTP/c2_code` folder and update the `key_path` and `cert_path` variables to have the `names` of those files.
|
||||
If you want to use SSL within this container specifically, then you can put your key and cert in the `C2_Profiles/HTTP/c2_code` folder and update the `key_path` and `cert_path` variables to have the `names` of those files. Alternatively, if you specify `use_ssl` as true and you don't have any certs already placed on disk, then the profile will automatically generate some self-signed certs for you to use.
|
||||
You should get a notification when the server starts with information about the configuration:
|
||||
|
||||
```
|
||||
@@ -67,11 +68,8 @@ A note about debugging:
|
||||
- It's recommended to have it on initially to help troubleshoot payload connectivity and configuration issues, but then to set it to `false` for actual operations
|
||||
|
||||
### Profile Options
|
||||
#### Base64 of a 32-byte AES Key
|
||||
Base64 value of the AES pre-shared key to use for communication with the agent. This will be auto-populated with a random key per payload, but you can also replace this with the base64 of any 32 bytes you want. If you don't want to use encryption here, blank out this value.
|
||||
|
||||
#### User Agent
|
||||
This is the user-agent string the agent will use when making HTTP requests.
|
||||
#### crypto type
|
||||
Indicate if you want to use no crypto (i.e. plaintext) or if you want to use Mythic's aes256_hmac. Using no crypto is really helpful for agent development so that it's easier to see messages and get started faster, but for actual operations you should leave the default to aes256_hmac.
|
||||
|
||||
#### Callback Host
|
||||
The URL for the redirector or Mythic server. This must include the protocol to use (e.g. `http://` or `https://`)
|
||||
@@ -85,8 +83,8 @@ Percentage of jitter effect for callback interval.
|
||||
#### Callback Port
|
||||
Number to specify the port number to callback to. This is split out since you don't _have_ to connect to the normal port (i.e. you could connect to http on port 8080).
|
||||
|
||||
#### Host header
|
||||
Value for the `Host` header in web requests. This is used with _Domain Fronting_.
|
||||
#### Headers
|
||||
This is an array of headers you want to include in your agent for HTTP comms. By default, this includes a standard User-Agent value and an option to add in a Host header for domain fronting. You can then add in as many other custom headers as you want.
|
||||
|
||||
#### Kill Date
|
||||
Date for the agent to automatically exit, typically the after an assessment is finished.
|
||||
Reference in New Issue
Block a user