reworked file browser to better handle large data, registration now creates deactivated accounts until an admin activates them, postgres password now randomized on first start in .env file and shared via docker-compose, can now filter by tasks with comments, payloads management now can show automatically generated payloads via a toggle, further reduced what operators can do without an active operation

This commit is contained in:
Cody Thomas
2020-11-11 17:40:37 -08:00
parent efaec3ce0b
commit 172d011821
38 changed files with 498 additions and 253 deletions
+2 -1
View File
@@ -1 +1,2 @@
COMPOSE_PROJECT_NAME=apfell
COMPOSE_PROJECT_NAME=mythic
POSTGRES_PASSWORD=super_secret_mythic_user_password
+1 -1
View File
@@ -9,7 +9,7 @@ A cross-platform, post-exploit, red teaming framework built with python3, docker
* Objective By the Sea 2019 talk on JXA: https://objectivebythesea.com/v2/talks/OBTS_v2_Thomas.pdf
* Objective By the sea 2019 Video: https://www.youtube.com/watch?v=E-QEsGsq3uI&list=PLliknDIoYszvTDaWyTh6SYiTccmwOsws8&index=17
* Current Version: 2.1.13
* Current Version: 2.1.14
## Documentation
+4
View File
@@ -9,6 +9,8 @@ services:
labels:
NAME: "mythic_postgres"
restart: on-failure
environment:
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
rabbitmq:
build: ./rabbitmq-docker
container_name: mythic_rabbitmq
@@ -27,6 +29,8 @@ services:
- ./mythic-docker:/Mythic
labels:
NAME: "mythic_server"
environment:
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
depends_on:
- postgres
- rabbitmq
+2 -2
View File
@@ -32,11 +32,11 @@ languageName = "English"
[[Languages.en.menu.shortcuts]]
name = "<i class='fab fa-fw fa-github'></i> GitHub repo"
identifier = "ds"
url = "https://github.com/its-a-feature/Apfell"
url = "https://github.com/its-a-feature/Mythic"
weight = 10
[[Languages.en.menu.shortcuts]]
name = "<i class='fas fa-fw fa-bookmark'></i> Mythic Documentation"
identifier = "hugodoc"
url = "https://docs.apfell.net/"
url = "https://docs.mythic-c2.net/"
weight = 20
+2 -1
View File
@@ -8,6 +8,7 @@ import ujson as json
import asyncio
import logging
import uuid
import os
# --------------------------------------------
# --------------------------------------------
@@ -39,7 +40,7 @@ listen_ip = (
)
db_name = "mythic_db"
db_user = "mythic_user"
db_pass = "super_secret_mythic_user_password"
db_pass = os.environ['POSTGRES_PASSWORD']
max_log_count = (
1 # if log_size > 0, rotate and make a max of max_log_count files to hold logs
)
+8 -3
View File
@@ -320,6 +320,8 @@ async def get_access_log_data(request, user):
entries = 1000
page = 1
total_entries = 0
if user["current_operation"] == "":
return json({"status": "error", "error": "Must be part an of operation to view this"})
try:
if request.method == "POST":
data = request.json
@@ -369,7 +371,10 @@ async def get_access_log_data(request, user):
["auth:user", "auth:apitoken_user"], False
) # user or user-level api token are ok
async def download_access_log_data(request, user):
if os.path.exists("mythic_access.log"):
return await file("mythic_access.log", filename="mythic_access.log")
if user["current_operation"] != "":
if os.path.exists("mythic_access.log"):
return await file("mythic_access.log", filename="mythic_access.log")
else:
return json({"status": "error", "error": "file does not exist"})
else:
return json({"status": "error", "error": "file does not exist"})
return json({"status": "error", "error": "Only an admin"})
+5 -5
View File
@@ -35,7 +35,7 @@ async def create_artifact(request, user):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot add base artifacts"}
)
@@ -68,7 +68,7 @@ async def update_artifact(request, user, id):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot modify base artifacts"}
)
@@ -102,7 +102,7 @@ async def delete_artifact(request, user, id):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot remove base artifacts"}
)
@@ -309,7 +309,7 @@ async def remove_artifact_tasks(request, user, aid):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot remove task artifacts"}
)
@@ -339,7 +339,7 @@ async def create_artifact_task_manually(request, user):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot create task artifacts"}
)
+7 -3
View File
@@ -72,7 +72,7 @@ async def create_browserscript(request, user):
except Exception as e:
return json({"status": "error", "error": "failed to parse json"})
pieces = {}
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot create browserscripts"}
)
@@ -143,7 +143,7 @@ async def modify_browserscript(request, user, bid):
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
try:
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot modify browser scripts"}
)
@@ -317,7 +317,7 @@ async def remove_browserscript(request, user, bid):
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
try:
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot remove browser scripts"}
)
@@ -361,6 +361,10 @@ async def import_browserscript(request, user):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot modify browser scripts"}
)
# get json data
try:
if request.files:
+10 -8
View File
@@ -107,7 +107,7 @@ async def start_c2profile(request, info, user):
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
try:
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot start c2 profiles"}
)
@@ -134,7 +134,7 @@ async def stop_c2profile(request, info, user):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json({"status": "error", "error": "Spectators cannot stop c2 profiles"})
return await stop_c2profile_func(info, user["username"])
@@ -164,7 +164,7 @@ async def status_c2profile(request, info, user):
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
try:
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot query c2 profiles"}
)
@@ -201,6 +201,8 @@ async def download_container_file_for_c2profiles(request, info, user):
except Exception as e:
print(e)
return json({"status": "error", "error": "failed to find C2 Profile"})
if user["current_operation"] == "":
return json({"status": "error", "error": "Must be part of an operation to see this"})
try:
status = await send_c2_rabbitmq_message(
profile.name, "get_config", "", user["username"]
@@ -225,7 +227,7 @@ async def upload_container_file_for_c2profiles(request, info, user):
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
try:
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{
"status": "error",
@@ -263,7 +265,7 @@ async def delete_c2profile(request, info, user):
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
try:
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot delete c2 profiles"}
)
@@ -390,7 +392,7 @@ async def update_c2_profile(request, profile, user):
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
try:
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot modify c2 profiles"}
)
@@ -430,7 +432,7 @@ async def save_c2profile_parameter_value_instance(request, info, user):
)
data = request.json # all of the name,value pairs instances we want to save
try:
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{
"status": "error",
@@ -568,7 +570,7 @@ async def delete_c2profile_parameter_value_instance(request, instance_name, user
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{
"status": "error",
+27 -38
View File
@@ -32,6 +32,7 @@ import threading
from time import sleep as tsleep
import socket
from app.api.siem_logger import log_to_siem
import sys
@mythic.route(mythic.config["API_BASE"] + "/callbacks/", methods=["GET"])
@@ -264,8 +265,8 @@ async def parse_agent_message(data: str, request):
return "", 404, new_callback, agent_uuid
# now to parse out what we're doing, everything is decrypted at this point
# shuttle everything out to the appropriate api files for processing
if keep_logs:
logger.info("Agent -> Mythic: " + js.dumps(decrypted))
#if keep_logs:
# logger.info("Agent -> Mythic: " + js.dumps(decrypted))
# print(decrypted)
response_data = {}
if decrypted["action"] == "get_tasking":
@@ -321,7 +322,7 @@ async def parse_agent_message(data: str, request):
"type": "AES256",
}
else:
return "", 404, new_callback, UUID
return "", 404, new_callback, agent_uuid
# staging is it's own thing, so return here instead of following down
elif decrypted["action"] == "staging_dh":
response_data, staging_info = await staging_dh(decrypted, UUID)
@@ -332,13 +333,13 @@ async def parse_agent_message(data: str, request):
"type": "AES256",
}
else:
return "", 404, new_callback, UUID
return "", 404, new_callback, agent_uuid
elif decrypted["action"] == "update_info":
response_data = await update_callback(decrypted, UUID)
agent_uuid = UUID
else:
await send_all_operations_message("Unknown action:" + str(decrypted["action"]), "warning")
return "", 404, new_callback, ""
return "", 404, new_callback, agent_uuid
# now that we have the right response data, format the response message
if (
"delegates" in decrypted
@@ -373,8 +374,9 @@ async def parse_agent_message(data: str, request):
response_data["delegates"].append({d_uuid: del_message})
# special encryption will be handled by the appropriate stager call
# base64 ( UID + ENC(response_data) )
if keep_logs:
logger.info("Mythic -> Agent: " + js.dumps(response_data))
#if keep_logs:
# logger.info("Mythic -> Agent: " + js.dumps(response_data))
#print(response_data)
final_msg = await crypt.encrypt_message(response_data, enc_key, UUID)
#if enc_key["type"] is None:
# return (
@@ -387,11 +389,13 @@ async def parse_agent_message(data: str, request):
# data=js.dumps(response_data).encode(), key=enc_key["enc_key"]
# )
# return base64.b64encode(UUID.encode() + enc_data).decode(), 200
return final_msg, 200, new_callback, UUID
return final_msg, 200, new_callback, agent_uuid
except Exception as e:
print(sys.exc_info()[-1].tb_lineno)
print("callback.py: " + str(e))
await send_all_operations_message(f"Exception dealing with message: {str(decoded)}\nfrom {request.host} as {request.method} method with headers: \n{request.headers}",
"warning")
return "", 404, new_callback, UUID
return "", 404, new_callback, agent_uuid
@mythic.route(mythic.config["API_BASE"] + "/callbacks/", methods=["POST"])
@@ -405,7 +409,7 @@ async def create_manual_callback(request, user):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot create manual callbacks"}
)
@@ -992,7 +996,7 @@ async def remove_graph_edge(request, id, user):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot remove graph edges"}
)
@@ -1169,17 +1173,17 @@ async def send_socks_data(data, callback: Callback):
for d in data:
if callback.id in cached_socks:
msg = js.dumps(d).encode()
print("******* SENDING DATA BACK TO PROXYCHAINS *****")
print(msg)
#print("******* SENDING DATA BACK TO PROXYCHAINS *****")
#print(msg)
msg = int.to_bytes(len(msg), 4, "big") + msg
total_msg += msg
# cached_socks[callback.id]['socket'].sendall(int.to_bytes(len(msg), 4, "big"))
else:
print("****** NO CACHED SOCKS, MUST BE CLOSED *******")
#else:
#print("****** NO CACHED SOCKS, MUST BE CLOSED *******")
cached_socks[callback.id]["socket"].sendall(total_msg)
return {"status": "success"}
except Exception as e:
print("******** EXCEPTION IN SEND SOCKS DATA *****\n{}".format(str(e)))
#print("******** EXCEPTION IN SEND SOCKS DATA *****\n{}".format(str(e)))
return {"status": "error", "error": str(e)}
@@ -1193,9 +1197,9 @@ async def get_socks_data(callback: Callback):
#print("Just got socks data to give to agent")
except:
break
if len(data) > 0:
print("******* SENDING THE FOLLOWING TO THE AGENT ******")
print(data)
#if len(data) > 0:
#print("******* SENDING THE FOLLOWING TO THE AGENT ******")
#print(data)
return data
@@ -1222,27 +1226,12 @@ def thread_read_socks(port: int, callback_id: int) -> None:
elif len(size) == 0:
tsleep(1)
continue
else:
print(
"############# only got {} bytes ############".format(
str(len(size))
)
)
# print("now trying to read in: {} bytes".format(str(size)))
msg = sock.recv(size)
if len(msg) < size:
print("########### didn't read the full size ########")
# print("got socks data from user")
try:
cached_socks[callback_id]["queue"].append(js.loads(msg.decode()))
print("just read from proxychains and added to queue for agent to pick up")
#print("just read from proxychains and added to queue for agent to pick up")
except Exception as d:
print(
"*" * 10
+ "Got exception from appending data to cached_socks"
+ "*" * 10
)
print(d)
if callback_id not in cached_socks:
#print("*" * 10 + "Got closing socket" + "*" * 10)
sock.close()
@@ -1339,7 +1328,7 @@ async def update_callback_web(request, id, user):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json({"status": "error", "error": "Spectators cannot update callbacks"})
data = request.json
try:
@@ -1492,7 +1481,7 @@ async def remove_callback(request, id, user):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot make callbacks inactive"}
)
@@ -1561,7 +1550,7 @@ async def get_callback_keys(request, user, id):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json({"status": "error", "error": "Spectators cannot get callback keys"})
try:
query = await db_model.operation_query()
+11 -3
View File
@@ -19,6 +19,8 @@ async def get_all_commands(request, user):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["current_operation"] == "":
return json({"status": "error", "error": "Must be part of a current operation to see this"})
all_commands = []
query = await db_model.command_query()
commands = await db_objects.execute(
@@ -50,6 +52,8 @@ async def check_command(request, user, ptype, cmd):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["current_operation"] == "":
return json({"status": "error", "error": "Must be part of a current operation to see this"})
status = {"status": "success"}
cmd = unquote_plus(cmd)
try:
@@ -97,6 +101,8 @@ async def get_all_parameters_for_command(request, user, id):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["current_operation"] == "":
return json({"status": "error", "error": "Must be part of a current operation to see this"})
try:
query = await db_model.command_query()
command = await db_objects.get(query, id=id)
@@ -124,6 +130,8 @@ async def get_all_attack_mappings_for_command(request, user, id):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["current_operation"] == "":
return json({"status": "error", "error": "Must be part of a current operation to see this"})
try:
query = await db_model.command_query()
command = await db_objects.get(query, id=id)
@@ -149,7 +157,7 @@ async def remove_attack_mapping_for_command(request, user, id, t_num):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot remove MITRE mappings"}
)
@@ -181,7 +189,7 @@ async def create_attack_mappings_for_command(request, user, id, t_num):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot add MITRE mappings"}
)
@@ -217,7 +225,7 @@ async def adjust_attack_mappings_for_command(request, user, id, t_num):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json(
{"status": "error", "error": "Spectators cannot modify MITRE mappings"}
)
+1 -1
View File
@@ -49,7 +49,7 @@ async def create_credential(request, user):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["view_mode"] == "spectator":
if user["view_mode"] == "spectator" or user["current_operation"] == "":
return json({"status": "error", "error": "Spectators cannot add credentials"})
if user["current_operation"] != "":
try:
+2
View File
@@ -316,6 +316,7 @@ async def create_filemeta_in_database_func(data):
operation = task.callback.operation
except Exception as e:
print("{} {}".format(str(sys.exc_info()[-1].tb_lineno), str(e)))
print("file_api.py")
return {"status": "error", "error": "failed to find task"}
try:
if "full_path" in data and data["full_path"] != "":
@@ -387,6 +388,7 @@ async def create_filemeta_in_database_func(data):
await log_to_siem(task.to_json(), mythic_object="file_screenshot")
except Exception as e:
print("{} {}".format(str(sys.exc_info()[-1].tb_lineno), str(e)))
print("file_api.py")
return {"status": "error", "error": "failed to create file"}
status = {"status": "success"}
return {**status, **filemeta.to_json()}
+59 -53
View File
@@ -54,54 +54,17 @@ async def get_filebrowser_tree_for_operation(operation_name):
try:
query = await db_model.filebrowserobj_query()
objs = await db_objects.execute(
query.where(db_model.FileBrowserObj.operation == operation)
)
output = {}
roots = []
sorted_obj = []
for o in objs:
ojson = o.to_json()
ojson["files"] = []
try:
for f in o.files:
fjson = f.to_json()
if f.task is not None and f.task.comment != "":
fjson["comment"] = f.task.comment
ojson["files"].append(fjson)
except Exception as e:
pass
if ojson["parent"] is None:
roots.append(ojson)
else:
sorted_obj.append(ojson)
sorted_obj = sorted(sorted_obj, key=lambda i: i["parent"])
for r in roots:
# print(r)
if r["host"] not in output:
output[r["host"]] = {}
# this is a root level node, there can be multiple
output[r["host"]][r["name"]] = treelib.Tree()
# print("adding root")
# print(r)
output[r["host"]][r["name"]].create_node(r["name"], r["id"], data=r)
# print(output)
# print("now to add in the children")
for o in sorted_obj:
for k, v in output[o["host"]].items():
if o["parent_path"].startswith(k):
# print("trying to add")
# print(o)
output[o["host"]][k].create_node(
o["name"], o["id"], parent=o["parent"], data=o
)
query.where(
(db_model.FileBrowserObj.operation == operation) &
(db_model.FileBrowserObj.is_file == False) &
(db_model.FileBrowserObj.parent == None)
))
final_output = {}
for k, v in output.items():
final_output[k] = {
"children": [],
"data": {"host": k, "is_file": False, "name": k},
}
for root, t in v.items():
final_output[k]["children"].append(js.loads(t.to_json(with_data=True)))
for e in objs:
e_json = e.to_json()
if e_json["host"] not in final_output:
final_output[e_json["host"]] = []
final_output[e_json["host"]].append(e_json)
return {"status": "success", "output": final_output}
except Exception as e:
print(e)
@@ -119,7 +82,7 @@ async def store_response_into_filebrowserobj(operation, task, response):
"status": "error",
"error": "Failed to parse and handle file browser objects",
}
if "host" not in response:
if "host" not in response or response["host"] == "" or response["host"] is None:
response["host"] = task.callback.host
# now that we have the immediate parent and all parent hierarchy create, deal with current obj and sub objects
try:
@@ -223,11 +186,14 @@ async def store_response_into_filebrowserobj(operation, task, response):
return {"status": "success"}
except Exception as e:
print(sys.exc_info()[-1].tb_lineno)
print(e)
print("file_browser_api.py: " + str(e))
return {"status": "error", "error": str(e)}
async def add_upload_file_to_file_browser(operation, task, file, data):
if "full_path" not in data or data["full_path"] is None or data["full_path"] == "":
return
data["is_file"] = True
data["permissions"] = {}
data["success"] = True
@@ -241,17 +207,18 @@ async def add_upload_file_to_file_browser(operation, task, file, data):
full_path = PureWindowsPath(data["full_path"])
data["name"] = full_path.name
data["parent_path"] = str(full_path.parents[0])
if "host" not in data:
if "host" not in data or data["host"] is None or data["host"] == "":
data["host"] = file.host
await store_response_into_filebrowserobj(operation, task, data)
try:
await store_response_into_filebrowserobj(operation, task, data)
fbo_query = await db_model.filebrowserobj_query()
fbo = await db_objects.get(fbo_query, operation=operation,
host=data["host"].encode("unicode-escape"),
full_path=data["full_path"].encode("unicode-escape"))
file.file_browser = fbo
except Exception as e:
print(str(e))
print(sys.exc_info()[-1].tb_lineno)
print("file_browser_api.py: " + str(e))
return
@@ -323,7 +290,7 @@ async def create_and_check_parents(operation, task, response):
return parent_obj
except Exception as e:
print(sys.exc_info()[-1].tb_lineno)
print(e)
print("file_browser_api.py: " + str(e))
return None
@@ -543,3 +510,42 @@ async def get_filebrowsobj_permissions_by_path(request, user):
"error": "failed to find that file browsing object in your current operation",
}
)
@mythic.route(mythic.config["API_BASE"] + "/filebrowserobj/<fid:int>/files", methods=["GET"])
@inject_user()
@scoped(
["auth:user", "auth:apitoken_user"], False
) # user or user-level api token are ok
async def get_filebrowsobj_files(request, user, fid):
if user["auth"] not in ["access_token", "apitoken"]:
abort(
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
try:
query = await db_model.operation_query()
operation = await db_objects.get(query, name=user["current_operation"])
query = await db_model.filebrowserobj_query()
files = await db_objects.execute(query.where(
(db_model.FileBrowserObj.operation == operation) &
(db_model.FileBrowserObj.parent == fid)
))
except Exception as e:
return json(
{
"status": "error",
"error": "failed to find that file browsing object in your current operation",
}
)
try:
output = []
for f in files:
f_json = f.to_json()
if f.is_file:
output.append({f_json["name"]: {"data": f_json}})
else:
output.append({f_json["name"]: {"data": f_json, "children": []}})
return json({"status": "success", "files": output})
except Exception as e:
return json({"status": "error", "error": str(e)})
+5 -2
View File
@@ -60,6 +60,8 @@ async def create_operator(request, user):
data = request.json
if user["view_mode"] == "spectator":
return json({"status": "error", "error": "Spectators cannot create users"})
if not user["admin"]:
return json({"status": "error", "error": "Only admins can create new users"})
if "username" not in data or data["username"] == "":
return json({"status": "error", "error": '"username" field is required'})
if not isinstance(data["username"], str) or not len(data["username"]):
@@ -70,11 +72,10 @@ async def create_operator(request, user):
}
)
password = await crypto.hash_SHA512(data["password"])
admin = False # cannot create a user initially as admin
# we need to create a new user
try:
new_operator = await db_objects.create(
Operator, username=data["username"], password=password, admin=admin
Operator, username=data["username"], password=password, admin=False
)
success = {"status": "success"}
new_user = new_operator.to_json()
@@ -237,6 +238,8 @@ async def remove_operator(request, oid, user):
return json({"status": "error", "error": "failed to find operator"})
try:
op.deleted = True
op.active = False
op.admin = False
await db_objects.update(op)
success = {"status": "success"}
return json({**success, **op.to_json()})
+8
View File
@@ -31,6 +31,8 @@ async def get_all_payloadtypes(request, user):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["current_operation"] == "":
return json({"status": "error", "error": "Must be part of an operation to view this"})
query = await db_model.payloadtype_query()
wrapquery = await db_model.wrappedpayloadtypes_query()
payloads = await db_objects.execute(
@@ -80,6 +82,8 @@ async def get_one_payloadtype(request, user, ptype):
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
try:
if user["current_operation"] == "":
return json({"status": "error", "error": "Must be part of an operation to view this"})
query = await db_model.payloadtype_query()
payloadtype = await db_objects.get(query, id=ptype)
query = await db_model.buildparameter_query()
@@ -121,6 +125,8 @@ async def update_one_payloadtype(request, user, ptype):
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
try:
if user["current_operation"] == "":
return json({"status": "error", "error": "Not part of an operation"})
query = await db_model.payloadtype_query()
payloadtype = await db_objects.get(query, id=ptype)
data = request.json
@@ -190,6 +196,8 @@ async def get_commands_for_payloadtype(request, user, ptype):
status_code=403,
message="Cannot access via Cookies. Use CLI or access via JS in browser",
)
if user["current_operation"] == "":
return json({"status": "error", "error": "Must be part of a current operation to see this"})
try:
query = await db_model.payloadtype_query()
payloadtype = await db_objects.get(query, id=ptype)
+9
View File
@@ -1086,6 +1086,12 @@ async def rabbit_heartbeat_callback(message: aio_pika.IncomingMessage):
# just listen for c2 heartbeats and update the database as necessary
async def start_listening():
logger.debug("starting to consume rabbitmq messages")
task = None
task2 = None
task3 = None
task4 = None
task5 = None
tasks = [task, task2, task3, task4, task5]
try:
task = asyncio.ensure_future(connect_and_consume_c2())
task2 = asyncio.ensure_future(connect_and_consume_heartbeats())
@@ -1094,6 +1100,9 @@ async def start_listening():
task5 = asyncio.ensure_future(connect_and_consume_c2_rpc())
await asyncio.wait_for([task, task2, task3, task4, task5], None)
except Exception as e:
for t in tasks:
if t is not None:
task.cancel()
await asyncio.sleep(3)
+4 -3
View File
@@ -521,9 +521,10 @@ async def post_agent_response(agent_message, UUID):
if host != file_meta.host:
file_meta.host = host.encode("unicode-escape")
await db_objects.update(file_meta)
await add_upload_file_to_file_browser(task.callback.operation, task, file_meta,
{"host": host,
"full_path": parsed_response["full_path"]})
if file_meta.full_path != "":
await add_upload_file_to_file_browser(task.callback.operation, task, file_meta,
{"host": host,
"full_path": parsed_response["full_path"]})
except Exception as e:
print(str(e))
logger.exception(
+3 -1
View File
@@ -672,6 +672,8 @@ async def add_task_to_callback_func(data, cid, user, op, operation, cb):
# it's not registered, so check the free clear tasking
if data["command"] == "clear":
# this means we're going to be clearing out some tasks depending on our access levels
if "params" not in data:
data["params"] = ""
task = await db_objects.create(
Task,
callback=cb,
@@ -695,7 +697,7 @@ async def add_task_to_callback_func(data, cid, user, op, operation, cb):
+ ": "
+ t["command"]
+ " "
+ t["params"]
+ t["original_params"]
)
await db_objects.create(Response, task=task, response=rsp)
return {"status": "success", **task.to_json()}
+1 -1
View File
@@ -4,7 +4,7 @@ from jinja2 import Environment, PackageLoader
from sanic_jwt.decorators import scoped, inject_user
from app.routes.routes import respect_pivot
env = Environment(loader=PackageLoader("app", "templates"))
env = Environment(loader=PackageLoader("app", "templates"), autoescape=True)
@mythic.route("/payloads/", methods=["GET"])
+1 -1
View File
@@ -4,7 +4,7 @@ from jinja2 import Environment, PackageLoader
from sanic_jwt.decorators import scoped, inject_user
from app.routes.routes import respect_pivot
env = Environment(loader=PackageLoader("app", "templates"))
env = Environment(loader=PackageLoader("app", "templates"), autoescape=True)
@mythic.route("/reporting/full_timeline")
+8 -37
View File
@@ -41,7 +41,7 @@ import glob
from app.api.callback_api import start_all_socks_after_restart
import sys
env = Environment(loader=PackageLoader("app", "templates"))
env = Environment(loader=PackageLoader("app", "templates"), autoescape=True)
async def respect_pivot(my_links, request):
@@ -87,6 +87,7 @@ class Login(BaseEndpoint):
async def get(self, request):
form = LoginForm(request)
errors = {}
successful_creation = request.args.pop("success", False)
errors["username_errors"] = "<br>".join(form.username.errors)
errors["password_errors"] = "<br>".join(form.password.errors)
template = env.get_template("login.html")
@@ -94,6 +95,7 @@ class Login(BaseEndpoint):
links=await respect_pivot(links, request),
form=form,
errors=errors,
successful_creation=successful_creation,
config={},
view_utc_time=False,
http="https" if use_ssl else "http",
@@ -112,7 +114,7 @@ class Login(BaseEndpoint):
user = await db_objects.get(query, username=username)
if await user.check_password(password):
if not user.active:
form.username.errors = ["account is deactivated, cannot log in"]
form.username.errors = ["Account is not active, cannot log in"]
else:
try:
user.last_login = datetime.datetime.utcnow()
@@ -219,10 +221,8 @@ class Register(BaseEndpoint):
# we need to create a new user
try:
user = await db_objects.create(
Operator, username=username, password=password
Operator, username=username, password=password, admin=False, active=False
)
user.last_login = datetime.datetime.utcnow()
await db_objects.update(user) # update the last login time to be now
query = await db_model.operation_query()
operations = await db_objects.execute(query)
for o in operations:
@@ -233,47 +233,18 @@ class Register(BaseEndpoint):
message="New user {} created".format(user.username),
)
await set_default_scripts(user)
# print(result)
# generate JWT token to be stored in a cookie
access_token, output = await self.responses.get_access_token_output(
request,
{"user_id": user.id, "auth": "cookie"},
self.config,
self.instance,
)
refresh_token = await self.instance.auth.generate_refresh_token(
request, {"user_id": user.id, "auth": "cookie"}
)
output.update({self.config.refresh_token_name(): refresh_token})
# we want to make sure to store access/refresh token in JS before moving into the rest of the app
template = env.get_template("register.html")
content = template.render(
links=await respect_pivot(links, request),
form=form,
errors=errors,
access_token=access_token,
refresh_token=refresh_token,
config={},
view_utc_time=False,
http="https" if use_ssl else "http",
ws="wss" if use_ssl else "ws",
)
resp = response.html(content)
resp.cookies[self.config.cookie_access_token_name()] = access_token
resp.cookies[self.config.cookie_access_token_name()]["httponly"] = True
resp.cookies[self.config.cookie_refresh_token_name()] = refresh_token
resp.cookies[self.config.cookie_refresh_token_name()]["httponly"] = True
return resp
return response.redirect("/login?success=true")
except Exception as e:
# failed to insert into database
print(e)
form.username.errors = ["Username already exists"]
form.username.errors = ["Failed to create user"]
errors["username_errors"] = "<br>".join(form.username.errors)
template = env.get_template("register.html")
content = template.render(
links=await respect_pivot(links, request),
form=form,
errors=errors,
successful_creation=False,
config={},
view_utc_time=False,
http="https" if use_ssl else "http",
+1 -1
View File
@@ -4,7 +4,7 @@ from jinja2 import Environment, PackageLoader
from sanic_jwt.decorators import scoped, inject_user
from app.routes.routes import respect_pivot
env = Environment(loader=PackageLoader("app", "templates"))
env = Environment(loader=PackageLoader("app", "templates"), autoescape=True)
@mythic.route("/services/host_file", methods=["GET"])
+6 -4
View File
@@ -913,8 +913,8 @@ async def ws_payloads_current_operation(request, ws, user):
payloads = await db_objects.execute(
query.where(
(Payload.operation == operation)
& (Payload.deleted == False)
& (Payload.auto_generated == False)
#& (Payload.deleted == False)
#& (Payload.auto_generated == False)
).order_by(Payload.id)
)
for p in payloads:
@@ -1362,13 +1362,15 @@ async def ws_commands(request, ws):
# basic info of just new commmands for the payload types page
@mythic.websocket("/ws/commands")
@inject_user()
@scoped(
["auth:user", "auth:apitoken_user"], False
) # user or user-level api token are ok
async def ws_commands(request, ws):
async def ws_commands(request, ws, user):
if not await valid_origin_header(request):
return
if user["current_operation"] == "":
return
try:
async with aiopg.create_pool(mythic.config["DB_POOL_CONNECT_STRING"]) as pool:
async with pool.acquire() as conn:
+1 -1
View File
@@ -725,7 +725,7 @@ li span.badge {
</li>
</ul>
</span>
<font size="4" style="float:right;padding-right:10px">v2.1.13</font>
<font size="4" style="float:right;padding-right:10px">v2.1.14</font>
{% endif %}
</div>
</nav>
+13 -8
View File
@@ -380,9 +380,6 @@
</div>
<div :key="'outputcontent' + task.id" class="collapse" :id="'cardbody' + task.id" >
<div :key="'outputcontentcardbody' + task.id" class="response-background card-body shadow" style="padding-top:0;padding-bottom: 0" >
<div :key="'showparams' + task.id" v-if="task.show_params">
Modified Parameters: &nbsp;[[task.params]]
</div>
<span :key="'usescripted' + task.id" v-if="task.hasOwnProperty('scripted') && task.use_scripted" v-html="task.scripted"></span>
<span v-else :key="'useregular' + task.id" class="card-text" v-for="rsp in task.response" :key="rsp.id" :id="'response' + rsp.id" style="white-space: pre-wrap"><span class="response"><pre>[[rsp.response]]</pre></span></span>
</div>
@@ -586,7 +583,7 @@
<i class="far fa-file"></i>
</template>
<template v-else>
<i class="fas fa-folder" :style="Object.values(f)[0].children !== undefined ? 'color:#f1d592' : ''"></i>
<i class="fas fa-folder" style="color:#f1d592"></i>
</template>
<template v-if="Object.values(f)[0].data.deleted">
<span style="text-decoration:line-through">[[Object.values(f)[0].data.name]] (deleted from target)</span>
@@ -600,7 +597,7 @@
<template v-else-if="Object.values(f)[0].data.success === false">
<i style="color:red" class="fas fa-exclamation-circle"></i>
</template>
<template v-if="Object.values(f)[0]['data']['files'].length > 0">
<template v-if="Object.values(f)[0]['data']['files'] !== undefined && Object.values(f)[0]['data']['files'].length > 0">
<a class="btn btn-md" :href='get_latest_download_path(Object.values(f)[0]["data"]["files"])' style="color:green"><i class="fas fa-download fa-lg"></i></a>
<span style="color:indianred">[[ get_latest_download_chunk_count(Object.values(f)[0]["data"]["files"]) ]]</span>
</template>
@@ -726,11 +723,12 @@
<button type="button" class="close" data-dismiss="modal">×</button>
</div>
<div class="modal-body">
Changes go away when you leave the page and are only viewable to you.
Filter displayed tasks by the operator who entered them, the task number, and/or the command itself.<br>
These filters are only visible to you and will disappear when you leave the page.
<table class="table-striped {{config['table-color']}} table" style="width:100%">
<tr>
<th style="width:7rem">Component</th>
<th style="width:4rem">Filter</th>
<th style="width:4rem">Enabled</th>
<th>Values</th>
</tr>
<tr>
@@ -743,7 +741,7 @@
</td>
</tr>
<tr>
<td>Task</td>
<td>Task Number</td>
<td><span class="material-switch pull-right"><input class="form-control" type="checkbox" :checked="task_filters['task']['active']" v-model="task_filters['task']['active']" id="task_filter_task"><label for="task_filter_task" class="btn-warning"></label></span></td>
<td>
<template v-if="task_filters['task']['active']">
@@ -761,6 +759,13 @@
</template>
</td>
</tr>
<tr>
<td>Has Comment</td>
<td><span class="material-switch pull-right"><input class="form-control" type="checkbox" :checked="task_filters['comment']['active']" v-model="task_filters['comment']['active']" id="task_filter_comment"><label for="task_filter_comment" class="btn-warning"></label></span></td>
<td>
Only show tasks with comments
</td>
</tr>
</table>
</div>
<div class="modal-footer">
+122 -57
View File
@@ -1532,7 +1532,8 @@ var task_data = new Vue({
task_filters: {
"task": {"active": false, "range_low": 0, "range_high": 1000000},
"operator": {"active": false, "username": ""},
"command": {"active": false, "cmd": ""}
"command": {"active": false, "cmd": ""},
"comment": {"active": false}
},
input_field_placeholder: {"data": "", "cid": -1},
meta: meta,
@@ -1943,6 +1944,8 @@ var task_data = new Vue({
});
$('#cardbody' + task.id).unbind('hidden.bs.collapse').on('hidden.bs.collapse', function () {
all_tasks[task.callback][task.id]['expanded'] = false;
Vue.set(all_tasks[task.callback][task.id], "response", {});
Vue.set(all_tasks[task.callback][task.id], "scripted", "");
$('#color-arrow' + task.id).removeClass('fa-minus').addClass('fa-plus');
});
},
@@ -2258,19 +2261,22 @@ var task_data = new Vue({
apply_filter: function (task) {
// determine if the specified task should be displayed based on the task_filters set
let status = true;
if (this.task_filters['task']['active'] && task.id !== undefined) {
if (this.task_filters['task']['active'] && task.id !== null) {
status = status && task.id <= this.task_filters['task']['range_high'] && task.id >= this.task_filters['task']['range_low'];
}
if (this.task_filters['operator']['active'] && task.operator !== undefined) {
if (this.task_filters['operator']['active'] && task.operator !== null) {
status = status && task.operator.includes(this.task_filters['operator']['username']);
}
if (this.task_filters['command']['active']) {
if (task.command === undefined) {
status = status && task.params.includes(this.task_filters['command']['cmd']);
if (task.command === null) {
status = status && task.original_params.includes(this.task_filters['command']['cmd']);
} else {
status = status && task.command.includes(this.task_filters['command']['cmd']);
}
}
if (this.task_filters['comment']['active']){
status = status && task.comment !== "";
}
// if nothing is active, default to true
return status;
},
@@ -2291,7 +2297,7 @@ var task_data = new Vue({
task_data.path = "/";
} else if (data['parent_path'] === undefined || data['parent_path'] === "") {
data['parent_path'] = "";
task_data.path = "";
task_data.path = data['name'];
} else if (data['parent_path'][0] === "/") {
if (data['parent_path'] === "/") {
task_data.path = data['parent_path'] + data['name'];
@@ -2299,9 +2305,14 @@ var task_data = new Vue({
task_data.path = data['parent_path'] + "/" + data['name'];
}
} else {
task_data.path = data['parent_path'] + "\\" + data['name'];
if(data['parent_path'][data['parent_path'].length -1] === "\\"){
task_data.path = data['parent_path'] + data['name'];
}else{
task_data.path = data['parent_path'] + "\\" + data['name'];
}
}
task_data.folder = {"data": data, "children": children};
Vue.set(task_data, "folder", {"data": data, "children": children});
},
update_file_browser_comment_live: function (data) {
httpGetAsync("{{http}}://{{links.server_ip}}:{{links.server_port}}{{links.api_base}}/filebrowserobj/" + data.id, (response) => {
@@ -2487,8 +2498,6 @@ var task_data = new Vue({
} else {
if(data.data.is_file){
alertTop("info", "Cannot view children of files, only folders.");
}else{
alertTop("info", "Children of folder unknown.");
}
}
}
@@ -2602,7 +2611,10 @@ Vue.component('browser-menu', {
' <span v-for="n in depth" style="color:gray">&nbsp;&nbsp;|&nbsp;</span>' +
' <template> <i :class="iconClasses" @click="toggleChildren" ></i>\n' +
" <span @click='test' class='browser-name' :style='data.deleted ? \"text-decoration:line-through\" : \"\" '> [[ data.name ]] </span>" +
' <template v-if="data.success === true">' +
' <template v-if="data.loading === true">' +
' <div class="spinner-border text-info" style="width:1rem;height:1rem" role="status"> </div> fetching...' +
' </template>' +
' <template v-else-if="data.success === true">' +
' <i style="color:green" class="fas fa-check"></i>' +
' </template>' +
' <template v-else-if="data.success === false">' +
@@ -2643,7 +2655,7 @@ Vue.component('browser-menu', {
},
labelClasses() {
let cls = "";
if (this.children !== undefined) {
if (!this.data.is_file) {
cls += ' has-children ';
}
return cls;
@@ -2662,14 +2674,59 @@ Vue.component('browser-menu', {
task_data.$forceUpdate();
return;
}
if(this.children !== undefined){
if(this.depth === 0){
this.$forceUpdate();
task_data.setBrowserTableData(this.data, this.children);
this.$forceUpdate();
}else{
alertTop("info", "Children of folder unknown, showing parent folder instead.");
task_data.setBrowserTableData(this.$parent.data, this.$parent.children);
this.$forceUpdate();
task_data.$forceUpdate();
return;
}
let me = this;
me.data.loading = true;
me.$forceUpdate();
httpGetAsync("{{http}}://{{links.server_ip}}:{{links.server_port}}{{links.api_base}}/filebrowserobj/" + this.data.id + "/files", (response) => {
try {
let info = JSON.parse(response);
if (info["status"] === "success") {
setTimeout(() => {
Vue.nextTick().then(function () {
if(me.children === undefined){
me.children = [];
}
for(let i = 0; i < info["files"].length; i++){
let found = false;
for(let j = 0; j < me.children.length; j++){
if(Object.values(me.children[j])[0].data.id === Object.values(info["files"][i])[0].data.id){
found = true;
break;
}
}
if(!found){
me.children.push(info["files"][i]);
}
}
task_data.setBrowserTableData(me.data, me.children);
setTimeout(() => { // setTimeout to put this into event queue
// executed after render
Vue.nextTick().then(function () {
me.data.loading = false;
me.$forceUpdate();
});
}, 0);
});
});
} else {
alertTop("danger", info["error"]);
me.data.loading = false;
me.$forceUpdate();
}
} catch (error) {
me.data.loading = false;
console.log(error.toString());
me.$forceUpdate();
alertTop("danger", "error fetching files in that folder.");
}
},
"GET", null);
},
get_latest_download_path() {
return "{{http}}://{{links.server_ip}}:{{links.server_port}}{{links.api_base}}/files/download/" + this.data['files'][0]['agent_file_id'];
@@ -3284,7 +3341,7 @@ function add_new_response(rsp, from_websocket) {
all_tasks[rsp['task']['callback']][rsp['task']['id']]['expanded'] = true;
//now that the new response has been added, potentially update the scripted version
if (Object.prototype.hasOwnProperty.call(browser_scripts, rsp['task']['command_id']) && all_tasks[rsp['task']['callback']][rsp['task']['id']]['use_scripted']) {
all_tasks[rsp['task']['callback']][rsp['task']['id']]['scripted'] = browser_scripts[rsp['task']['command_id']](rsp['task'], Object.values(all_tasks[rsp['task']['callback']][rsp['task']['id']]['response']));
Vue.set(all_tasks[rsp['task']['callback']][rsp['task']['id']], 'scripted', browser_scripts[rsp['task']['command_id']](rsp['task'], Object.values(all_tasks[rsp['task']['callback']][rsp['task']['id']]['response'])));
}
if (from_websocket) {
//we want to make sure we have this expanded by default
@@ -4027,45 +4084,15 @@ function startwebsocket_filebrowser() {
} else if (event.data !== "") {
let data = JSON.parse(event.data);
if (!initial_group_done) {
Vue.set(meta, "file_browser", data);
for(const [key, value] of Object.entries(data)){
for(let i = 0; i < value.length; i++){
process_file_browser_data(value[i]);
}
}
initial_group_done = true;
} else {
// now we have streaming updates and need to merge them in
// first step is to find the right host
if (data['host'] === undefined || data['host'].length === 0) {
return
}
if (!Object.prototype.hasOwnProperty.call(meta['file_browser'], data['host'])) {
Vue.set(meta["file_browser"], data["host"], {
"data": {
"name": data['host'],
"host": data['host']
}, "children": []
});
}
// what if we're adding a new top level root
if (data['parent'] === null) {
for (let i = 0; i < meta['file_browser'][data['host']]['children'].length; i++) {
if (data['name'] in meta['file_browser'][data['host']]['children'][i]) {
Object.assign(meta['file_browser'][data['host']]['children'][i][data['name']]['data'],
meta['file_browser'][data['host']]['children'][i]['data'],
data);
task_data.$forceUpdate();
return;
}
}
// if we get here, we have a root element that's new, so add it
let new_data = {};
new_data[data['name']] = {"data": data, "children": []};
meta['file_browser'][data['host']]['children'].push(new_data);
task_data.$forceUpdate();
} else {
add_update_file_browser(data, meta['file_browser'][data['host']]);
task_data.$forceUpdate();
}
setTimeout(() => { // setTimeout to put this into event queue
// executed after render
task_data.$forceUpdate();
}, 0);
process_file_browser_data(data);
}
} else {
initial_group_done = true;
@@ -4079,6 +4106,44 @@ function startwebsocket_filebrowser() {
};
}
function process_file_browser_data(data){
// first step is to find the right host
if (data['host'] === undefined || data['host'].length === 0) {
return
}
if (!Object.prototype.hasOwnProperty.call(meta['file_browser'], data['host'])) {
Vue.set(meta["file_browser"], data["host"], {
"data": {
"name": data['host'],
"host": data['host']
}, "children": []
});
}
// what if we're adding a new top level root
if (data['parent'] === null) {
for (let i = 0; i < meta['file_browser'][data['host']]['children'].length; i++) {
if (data['name'] in meta['file_browser'][data['host']]['children'][i]) {
Object.assign(meta['file_browser'][data['host']]['children'][i][data['name']]['data'],
meta['file_browser'][data['host']]['children'][i]['data'],
data);
task_data.$forceUpdate();
return;
}
}
// if we get here, we have a root element that's new, so add it
let new_data = {};
new_data[data['name']] = {"data": data, "children": []};
meta['file_browser'][data['host']]['children'].push(new_data);
task_data.$forceUpdate();
} else {
add_update_file_browser(data, meta['file_browser'][data['host']]);
task_data.$forceUpdate();
}
setTimeout(() => { // setTimeout to put this into event queue
// executed after render
task_data.$forceUpdate();
}, 0);
}
function add_update_file_browser(search, element) {
@@ -4106,7 +4171,7 @@ function add_update_file_browser(search, element) {
//if we get here, and parent is true, then we are the parent and failed to find the child, so we need to add it
if (element['data']['id'] === search['parent']) {
let new_data = {};
new_data[search['name']] = {"data": search, "children": undefined};
new_data[search['name']] = {"data": search, "children": []};
if(element['children'] === undefined){
Vue.set(element, "children", []);
}
+5
View File
@@ -1,6 +1,11 @@
{% extends "base.html" %}
{% block body %}
<div style="position:absolute; top:20%; left:35%">
{% if successful_creation %}
<div class="alert alert-success" role="alert">
Successfully created a new user! An admin must activate your account before you can log in.
</div>
{% endif %}
<img src="/static/red_blue_login.png" style="height:calc(40vh)" class="center">
<form action="" method="post">
{{ errors.token_errors }}
@@ -4,6 +4,13 @@
<div class="card-header shadow border border-dark bg-header text-white">
<h2 style="display:inline-block">Created <span class="operator">Payloads</span></h2>
<button style="float:right" class="btn btn{{config['outline-buttons']}}danger btn-md" onclick="delete_selected_function()">Delete all Selected</button>
<template v-if="view_auto_generated">
<button style="float:right" class="btn btn{{config['outline-buttons']}}info btn-md" @click="toggle_auto_generated()">Hide Ephemeral</button>
</template>
<template v-else>
<button style="float:right" class="btn btn{{config['outline-buttons']}}success btn-md" @click="toggle_auto_generated()">Show Ephemeral</button>
</template>
</div>
<table class="table table-condensed table-striped {{config['table-color']}} border border-dark shadow" style="margin-bottom:0">
<tr>
@@ -14,21 +21,30 @@
<th onclick="sort_table(this)" style="text-align:left"><b>File</b></th>
<th onclick="sort_table(this)" style="text-align:left"><b>Description</b></th>
<th onclick="sort_table(this)" style="text-align:left;width:10rem"><b>Creator</b></th>
<th onclick="sort_table(this)" style="text-align:left;width:10rem"><b>Payload Type</b></th>
<th onclick="sort_table(this)" style="text-align:center;width:10rem"><b>Payload Type</b></th>
<template v-if="view_auto_generated">
<th onclick="sort_table(this, 'int')" style="text-align:center;width:10rem">
<b>Task</b>
</th>
</template>
<th style="width:12rem;text-align:center" onclick="sort_table(this, 'date')"><b>Creation Time</b></th>
</tr>
<!-- Repeat this for each payload -->
<tr v-for="p in payloads" :key="p.creation_time" v-show="!p.deleted">
<tr v-for="p in payloads" :key="p.creation_time" v-show="p.auto_generated == false || view_auto_generated">
<td style="text-align:center">
<input style="padding-right:10px" type="checkbox" v-model="p.checked"> <button type="button" class="btn btn{{config['outline-buttons']}}danger btn-sm" @click="delete_button(p)" ><i class="fas fa-trash-alt"></i></button>
</td>
<td style="text-align:center">
<div class="dropdown" style="display:inline">
<template v-if="p.auto_generated">
<button style="float:right" class="btn btn{{config['outline-buttons']}}secondary btn-sm" @click="show_parameters_button(p, false)">View Config</button>
</template>
<template v-else>
<div class="dropdown" style="display:inline">
<button class="btn btn{{config['outline-buttons']}}secondary dropdown-toggle btn-sm" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" :data-target="p.uuid">
<i class="fas fa-edit"> Payload Actions</i>
</button>
<div class="dropdown-menu" :id="p.uuid">
<button type="button" class="dropdown-item" @click="edit_filename(p)">Rename File</button>
<button type="button" class="dropdown-item" @click="edit_filename(p)">Rename File</button>
<button type="button" class="dropdown-item" @click="edit_description(p)">Edit Description</button>
<button type="button" class="dropdown-item" @click="update_callback_alert(p)">
<template v-if="p.callback_alert">Stop Alerting to new Callbacks</template>
@@ -38,10 +54,12 @@
<button type="button" class="dropdown-item" @click="show_parameters_button(p, true)">View Build Message</button>
<button type="button" class="dropdown-item" @click="register_manual_callback(p)">Manually Register Callback</button>
</div>
</div>
</div>
</template>
</td>
<td style="text-align:center">
<template v-if="p.deleted || p.build_phase !== 'success'">
<template v-if="p.deleted || p.build_phase !== 'success' || p.auto_generated">
<button class="btn btn{{config['outline-buttons']}}danger btn-sm" disabled type="button"><i class="fas fa-ban fa-lg"></i></button>
</template>
<template v-else>
@@ -62,7 +80,14 @@
<td>[[ p['file_id']['filename'] ]]</td>
<td>[[ p.tag ]]</td>
<td>[[ p.operator ]]</td>
<td style="text-align:left">[[ p['payload_type'] ]]</td>
<td style="text-align:center">[[ p['payload_type'] ]]</td>
<template v-if="view_auto_generated">
<td style="text-align:center">
<template v-if="p.task !== null">
<a :href="'{{http}}://{{links.server_ip}}:{{links.server_port}}/tasks/' + p['task']['id']" target="_blank">[[ p['task']['id'] ]]</a>
</template>
</td>
</template>
<td style="text-align:center">[[ toLocalTime(p.creation_time) ]]</td>
</tr>
<!-- End of the repeating -->
@@ -4,7 +4,8 @@ var payloads = []; //all services data
var payloads_table = new Vue({
el: '#payloads_table',
data: {
payloads
payloads,
view_auto_generated: false
},
methods: {
delete_button: function (p) {
@@ -130,6 +131,9 @@ var payloads_table = new Vue({
}, "PUT", {"description": edit_payload_vue.edit_value});
});
},
toggle_auto_generated: function(){
this.view_auto_generated = !this.view_auto_generated;
}
},
delimiters: ['[[', ']]']
@@ -266,6 +270,7 @@ function startwebsocket_payloads() {
} else if (event.data !== "") {
let pdata = JSON.parse(event.data);
if (pdata['deleted'] === false) {
console.log(pdata);
for (let i = 0; i < payloads_table.payloads.length; i++) {
if (pdata['id'] === payloads_table.payloads[i]['id']) {
//just update the data
+1 -2
View File
@@ -149,7 +149,7 @@
[[token.token_type]]
</td>
<td>
[[token.id]]: [[token.token_value]]
[[token.token_value]]
</td>
<td>
[[toLocalTime(token.creation_time)]]
@@ -179,7 +179,6 @@
</td>
<td>
<select class="form-control" id="apitokenCreateTokenType">
<option value="C2">C2</option>
<option value="User">User</option>
</select>
</td>
@@ -109,11 +109,12 @@
<button type="button" class="close" data-dismiss="modal">×</button>
</div>
<div class="modal-body">
Changes go away when you leave the page and are only viewable to you.
Filter displayed tasks by the operator who entered them, the task number, and/or the command itself.<br>
These filters are only visible to you and will disappear when you leave the page.
<table class="table-striped {{config['table-color']}} table" style="width:100%">
<tr>
<th style="width:7rem">Component</th>
<th style="width:4rem">Filter</th>
<th style="width:4rem">Enabled</th>
<th>Values</th>
</tr>
<tr>
@@ -126,7 +127,7 @@
</td>
</tr>
<tr>
<td>Task</td>
<td>Task Number</td>
<td><span class="material-switch pull-right"><input class="form-control" type="checkbox" :checked="task_filters['task']['active']" v-model="task_filters['task']['active']" id="task_filter_task"><label for="task_filter_task" class="btn-warning"></label></span></td>
<td>
<template v-if="task_filters['task']['active']">
@@ -144,6 +145,13 @@
</template>
</td>
</tr>
<tr>
<td>Has Comment</td>
<td><span class="material-switch pull-right"><input class="form-control" type="checkbox" :checked="task_filters['comment']['active']" v-model="task_filters['comment']['active']" id="task_filter_comment"><label for="task_filter_comment" class="btn-warning"></label></span></td>
<td>
Only show tasks with comments
</td>
</tr>
</table>
</div>
<div class="modal-footer">
+13 -5
View File
@@ -26,7 +26,8 @@ var callback_table = new Vue({
task_filters: {
"task": {"active": false, "range_low": 0, "range_high": 1000000},
"operator": {"active": false, "username": ""},
"command": {"active": false, "cmd": ""}
"command": {"active": false, "cmd": ""},
"comment": {"active": false}
}
},
methods: {
@@ -390,14 +391,21 @@ var callback_table = new Vue({
apply_filter: function (task) {
// determine if the specified task should be displayed based on the task_filters set
let status = true;
if (this.task_filters['task']['active'] && task.id !== undefined) {
if (this.task_filters['task']['active'] && task.id !== null) {
status = status && task.id <= this.task_filters['task']['range_high'] && task.id >= this.task_filters['task']['range_low'];
}
if (this.task_filters['operator']['active'] && task.operator !== undefined) {
if (this.task_filters['operator']['active'] && task.operator !== null) {
status = status && task.operator.includes(this.task_filters['operator']['username']);
}
if (this.task_filters['command']['active'] && task.command !== undefined) {
status = status && task.command.includes(this.task_filters['command']['cmd']);
if (this.task_filters['command']['active']) {
if (task.command === null) {
status = status && task.original_params.includes(this.task_filters['command']['cmd']);
} else {
status = status && task.command.includes(this.task_filters['command']['cmd']);
}
}
if (this.task_filters['comment']['active']){
status = status && task.comment !== "";
}
// if nothing is active, default to true
return status;
+3
View File
@@ -1 +1,4 @@
From itsafeaturemythic/mythic_postgres:0.0.1
COPY ["pg_hba.conf", "/var/lib/postgresql/pg_hba.conf"]
COPY ["configuration.sql", "/docker-entrypoint-initdb.d/configuration.sql"]
COPY ["configuration.sh", "/docker-entrypoint-initdb.d/configuration.sh"]
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
mv /var/lib/postgresql/pg_hba.conf /var/lib/postgresql/data/pg_hba.conf
+2
View File
@@ -0,0 +1,2 @@
\set user_pass '\'' `echo "$POSTGRES_PASSWORD"` '\'';
ALTER USER mythic_user PASSWORD :user_pass;
+91
View File
@@ -0,0 +1,91 @@
# PostgreSQL Client Authentication Configuration File
# ===================================================
#
# Refer to the "Client Authentication" section in the PostgreSQL
# documentation for a complete description of this file. A short
# synopsis follows.
#
# This file controls: which hosts are allowed to connect, how clients
# are authenticated, which PostgreSQL user names they can use, which
# databases they can access. Records take one of these forms:
#
# local DATABASE USER METHOD [OPTIONS]
# host DATABASE USER ADDRESS METHOD [OPTIONS]
# hostssl DATABASE USER ADDRESS METHOD [OPTIONS]
# hostnossl DATABASE USER ADDRESS METHOD [OPTIONS]
#
# (The uppercase items must be replaced by actual values.)
#
# The first field is the connection type: "local" is a Unix-domain
# socket, "host" is either a plain or SSL-encrypted TCP/IP socket,
# "hostssl" is an SSL-encrypted TCP/IP socket, and "hostnossl" is a
# plain TCP/IP socket.
#
# DATABASE can be "all", "sameuser", "samerole", "replication", a
# database name, or a comma-separated list thereof. The "all"
# keyword does not match "replication". Access to replication
# must be enabled in a separate record (see example below).
#
# USER can be "all", a user name, a group name prefixed with "+", or a
# comma-separated list thereof. In both the DATABASE and USER fields
# you can also write a file name prefixed with "@" to include names
# from a separate file.
#
# ADDRESS specifies the set of hosts the record matches. It can be a
# host name, or it is made up of an IP address and a CIDR mask that is
# an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that
# specifies the number of significant bits in the mask. A host name
# that starts with a dot (.) matches a suffix of the actual host name.
# Alternatively, you can write an IP address and netmask in separate
# columns to specify the set of hosts. Instead of a CIDR-address, you
# can write "samehost" to match any of the server's own IP addresses,
# or "samenet" to match any address in any subnet that the server is
# directly connected to.
#
# METHOD can be "trust", "reject", "md5", "password", "gss", "sspi",
# "ident", "peer", "pam", "ldap", "radius" or "cert". Note that
# "password" sends passwords in clear text; "md5" is preferred since
# it sends encrypted passwords.
#
# OPTIONS are a set of options for the authentication in the format
# NAME=VALUE. The available options depend on the different
# authentication methods -- refer to the "Client Authentication"
# section in the documentation for a list of which options are
# available for which authentication methods.
#
# Database and user names containing spaces, commas, quotes and other
# special characters must be quoted. Quoting one of the keywords
# "all", "sameuser", "samerole" or "replication" makes the name lose
# its special character, and just match a database or username with
# that name.
#
# This file is read on server startup and when the postmaster receives
# a SIGHUP signal. If you edit the file on a running system, you have
# to SIGHUP the postmaster for the changes to take effect. You can
# use "pg_ctl reload" to do that.
# Put your actual configuration here
# ----------------------------------
#
# If you want to allow non-local connections, you need to add more
# "host" records. In that case you will also need to make PostgreSQL
# listen on a non-local interface via the listen_addresses
# configuration parameter, or via the -i or -h command line switches.
# CAUTION: Configuring the system for local "trust" authentication
# allows any local user to connect as any PostgreSQL user, including
# the database superuser. If you do not trust all your local users,
# use another authentication method.
# TYPE DATABASE USER ADDRESS METHOD
# "local" is for Unix domain socket connections only
local all all md5
# IPv4 local connections:
#host all all 127.0.0.1/32 trust
# IPv6 local connections:
#host all all ::1/128 trust
# Allow replication connections from localhost, by a user with the
# replication privilege.
host all all all md5
+8
View File
@@ -141,6 +141,14 @@ then
exit 1
fi
fi
if [ "$(ls -A postgres-docker/database/)" ]; then
echo -e "${BLUE}[*]${NC} Database exists already"
else
postgres_password=$(tr -cd '[:alnum:]' < /dev/urandom | fold -w30 | head -n1)
base_replace="POSTGRES_PASSWORD=super_secret_mythic_user_password"
echo -e "${BLUE}[*]${NC} Replacing static database password with randomized one"
sed -i "s/$base_replace/POSTGRES_PASSWORD=$postgres_password/g" .env
fi
# start the main mythic components
docker-compose up --build -d
if [ $? -ne 0 ]