From 172d011821c008000aa4beef0185406b767c19ad Mon Sep 17 00:00:00 2001 From: Cody Thomas Date: Wed, 11 Nov 2020 17:40:37 -0800 Subject: [PATCH] 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 --- .env | 3 +- README.md | 2 +- docker-compose.yml | 4 + documentation-docker/config.toml | 4 +- mythic-docker/app/__init__.py | 3 +- mythic-docker/app/api/analytics_api.py | 11 +- mythic-docker/app/api/artifacts_api.py | 10 +- mythic-docker/app/api/browserscript_api.py | 10 +- mythic-docker/app/api/c2profiles_api.py | 18 +- mythic-docker/app/api/callback_api.py | 65 +++---- mythic-docker/app/api/command_api.py | 14 +- mythic-docker/app/api/credential_api.py | 2 +- mythic-docker/app/api/file_api.py | 2 + mythic-docker/app/api/file_browser_api.py | 112 +++++------ mythic-docker/app/api/operator_api.py | 7 +- mythic-docker/app/api/payloadtype_api.py | 8 + mythic-docker/app/api/rabbitmq_api.py | 9 + mythic-docker/app/api/response_api.py | 7 +- mythic-docker/app/api/task_api.py | 4 +- mythic-docker/app/routes/payloads_routes.py | 2 +- mythic-docker/app/routes/reporting_routes.py | 2 +- mythic-docker/app/routes/routes.py | 45 +---- mythic-docker/app/routes/services_routes.py | 2 +- mythic-docker/app/routes/websocket_routes.py | 10 +- mythic-docker/app/templates/base.html | 2 +- mythic-docker/app/templates/callbacks.html | 21 +- mythic-docker/app/templates/callbacks.js | 179 ++++++++++++------ mythic-docker/app/templates/login.html | 5 + .../app/templates/payload_management.html | 39 +++- .../app/templates/payload_management.js | 7 +- mythic-docker/app/templates/settings.html | 3 +- .../app/templates/split_callback.html | 14 +- mythic-docker/app/templates/split_callback.js | 18 +- postgres-docker/Dockerfile | 3 + postgres-docker/configuration.sh | 3 + postgres-docker/configuration.sql | 2 + postgres-docker/pg_hba.conf | 91 +++++++++ start_mythic.sh | 8 + 38 files changed, 498 insertions(+), 253 deletions(-) create mode 100644 postgres-docker/configuration.sh create mode 100644 postgres-docker/configuration.sql create mode 100755 postgres-docker/pg_hba.conf diff --git a/.env b/.env index 44490d30..56829c3a 100755 --- a/.env +++ b/.env @@ -1 +1,2 @@ -COMPOSE_PROJECT_NAME=apfell +COMPOSE_PROJECT_NAME=mythic +POSTGRES_PASSWORD=super_secret_mythic_user_password \ No newline at end of file diff --git a/README.md b/README.md index 4f4b59fa..d8c1200c 100755 --- a/README.md +++ b/README.md @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index ec6e9287..d305e45a 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/documentation-docker/config.toml b/documentation-docker/config.toml index e26e21c4..6a213287 100644 --- a/documentation-docker/config.toml +++ b/documentation-docker/config.toml @@ -32,11 +32,11 @@ languageName = "English" [[Languages.en.menu.shortcuts]] name = " 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 = " Mythic Documentation" identifier = "hugodoc" -url = "https://docs.apfell.net/" +url = "https://docs.mythic-c2.net/" weight = 20 diff --git a/mythic-docker/app/__init__.py b/mythic-docker/app/__init__.py index c2f5ff29..0ba016e6 100755 --- a/mythic-docker/app/__init__.py +++ b/mythic-docker/app/__init__.py @@ -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 ) diff --git a/mythic-docker/app/api/analytics_api.py b/mythic-docker/app/api/analytics_api.py index 975c7d82..87488b77 100755 --- a/mythic-docker/app/api/analytics_api.py +++ b/mythic-docker/app/api/analytics_api.py @@ -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"}) diff --git a/mythic-docker/app/api/artifacts_api.py b/mythic-docker/app/api/artifacts_api.py index e95ae13a..3bd017df 100755 --- a/mythic-docker/app/api/artifacts_api.py +++ b/mythic-docker/app/api/artifacts_api.py @@ -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"} ) diff --git a/mythic-docker/app/api/browserscript_api.py b/mythic-docker/app/api/browserscript_api.py index 4e4e4876..98f15e6c 100755 --- a/mythic-docker/app/api/browserscript_api.py +++ b/mythic-docker/app/api/browserscript_api.py @@ -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: diff --git a/mythic-docker/app/api/c2profiles_api.py b/mythic-docker/app/api/c2profiles_api.py index 6dccb5cc..a0414edf 100755 --- a/mythic-docker/app/api/c2profiles_api.py +++ b/mythic-docker/app/api/c2profiles_api.py @@ -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", diff --git a/mythic-docker/app/api/callback_api.py b/mythic-docker/app/api/callback_api.py index 72d37380..e28ca86a 100755 --- a/mythic-docker/app/api/callback_api.py +++ b/mythic-docker/app/api/callback_api.py @@ -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() diff --git a/mythic-docker/app/api/command_api.py b/mythic-docker/app/api/command_api.py index 43c2a410..c35e0cd9 100755 --- a/mythic-docker/app/api/command_api.py +++ b/mythic-docker/app/api/command_api.py @@ -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"} ) diff --git a/mythic-docker/app/api/credential_api.py b/mythic-docker/app/api/credential_api.py index 18477c02..49d57b7f 100755 --- a/mythic-docker/app/api/credential_api.py +++ b/mythic-docker/app/api/credential_api.py @@ -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: diff --git a/mythic-docker/app/api/file_api.py b/mythic-docker/app/api/file_api.py index 528a319b..34ea4711 100755 --- a/mythic-docker/app/api/file_api.py +++ b/mythic-docker/app/api/file_api.py @@ -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()} diff --git a/mythic-docker/app/api/file_browser_api.py b/mythic-docker/app/api/file_browser_api.py index 1a80f1ec..b6d842f3 100644 --- a/mythic-docker/app/api/file_browser_api.py +++ b/mythic-docker/app/api/file_browser_api.py @@ -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//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)}) diff --git a/mythic-docker/app/api/operator_api.py b/mythic-docker/app/api/operator_api.py index f86ae586..de881b84 100755 --- a/mythic-docker/app/api/operator_api.py +++ b/mythic-docker/app/api/operator_api.py @@ -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()}) diff --git a/mythic-docker/app/api/payloadtype_api.py b/mythic-docker/app/api/payloadtype_api.py index 50c87ae2..d1480572 100755 --- a/mythic-docker/app/api/payloadtype_api.py +++ b/mythic-docker/app/api/payloadtype_api.py @@ -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) diff --git a/mythic-docker/app/api/rabbitmq_api.py b/mythic-docker/app/api/rabbitmq_api.py index 5acb8068..dfa36feb 100755 --- a/mythic-docker/app/api/rabbitmq_api.py +++ b/mythic-docker/app/api/rabbitmq_api.py @@ -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) diff --git a/mythic-docker/app/api/response_api.py b/mythic-docker/app/api/response_api.py index 5ee0fe4c..2d5433a1 100755 --- a/mythic-docker/app/api/response_api.py +++ b/mythic-docker/app/api/response_api.py @@ -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( diff --git a/mythic-docker/app/api/task_api.py b/mythic-docker/app/api/task_api.py index 0581d021..ba6bef11 100755 --- a/mythic-docker/app/api/task_api.py +++ b/mythic-docker/app/api/task_api.py @@ -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()} diff --git a/mythic-docker/app/routes/payloads_routes.py b/mythic-docker/app/routes/payloads_routes.py index 3067856d..c7185e36 100755 --- a/mythic-docker/app/routes/payloads_routes.py +++ b/mythic-docker/app/routes/payloads_routes.py @@ -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"]) diff --git a/mythic-docker/app/routes/reporting_routes.py b/mythic-docker/app/routes/reporting_routes.py index 7b3cc751..c4b67a5c 100755 --- a/mythic-docker/app/routes/reporting_routes.py +++ b/mythic-docker/app/routes/reporting_routes.py @@ -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") diff --git a/mythic-docker/app/routes/routes.py b/mythic-docker/app/routes/routes.py index 264d2158..e2b51aa0 100755 --- a/mythic-docker/app/routes/routes.py +++ b/mythic-docker/app/routes/routes.py @@ -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"] = "
".join(form.username.errors) errors["password_errors"] = "
".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"] = "
".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", diff --git a/mythic-docker/app/routes/services_routes.py b/mythic-docker/app/routes/services_routes.py index f8070fde..1f91a1e9 100755 --- a/mythic-docker/app/routes/services_routes.py +++ b/mythic-docker/app/routes/services_routes.py @@ -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"]) diff --git a/mythic-docker/app/routes/websocket_routes.py b/mythic-docker/app/routes/websocket_routes.py index 1fde01c2..88712afa 100755 --- a/mythic-docker/app/routes/websocket_routes.py +++ b/mythic-docker/app/routes/websocket_routes.py @@ -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: diff --git a/mythic-docker/app/templates/base.html b/mythic-docker/app/templates/base.html index ad545b74..fad4592f 100755 --- a/mythic-docker/app/templates/base.html +++ b/mythic-docker/app/templates/base.html @@ -725,7 +725,7 @@ li span.badge { - v2.1.13 + v2.1.14 {% endif %} diff --git a/mythic-docker/app/templates/callbacks.html b/mythic-docker/app/templates/callbacks.html index 1861e209..db552e65 100755 --- a/mythic-docker/app/templates/callbacks.html +++ b/mythic-docker/app/templates/callbacks.html @@ -380,9 +380,6 @@
-
- Modified Parameters:  [[task.params]] -
[[rsp.response]]
@@ -586,7 +583,7 @@