├── .github └── workflows │ └── docker.yml ├── .gitignore ├── C2_Profiles └── .keep ├── LICENSE.md ├── Payload_Type ├── __init__.py └── nemesis │ ├── .docker │ ├── Dockerfile │ └── requirements.txt │ ├── Dockerfile │ ├── main.py │ └── nemesis │ ├── NemesisRequests │ ├── NemesisAPI.py │ ├── NemesisAPIClasses.py │ └── __init__.py │ ├── __init__.py │ ├── agent_functions │ ├── __init__.py │ ├── builder.py │ ├── chromium.py │ ├── credentials.py │ ├── hashes.py │ ├── nemesis.svg │ ├── triage.py │ └── upload.py │ └── browser_scripts │ ├── chromium.js │ ├── credentials.js │ ├── hashes.js │ └── triage.js ├── README.md ├── agent_capabilities.json ├── agent_icons ├── .keep └── nemesis.svg ├── config.json ├── documentation-c2 └── .keep ├── documentation-payload └── .keep └── documentation-wrapper └── .keep /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | # Pulled from Thanatos (https://github.com/MythicAgents/thanatos/blob/rewrite/.github/workflows/image.yml) - MEhrn00 2 | 3 | # Name for the Github actions workflow 4 | name: Build and push container images 5 | 6 | on: 7 | # Only run workflow when there is a new release published in Github 8 | #release: 9 | # types: [published] 10 | push: 11 | branches: 12 | - 'master' 13 | - 'main' 14 | tags: 15 | - "v*.*.*" 16 | 17 | # Variables holding configuration settings 18 | env: 19 | # Container registry the built container image will be pushed to 20 | REGISTRY: ghcr.io 21 | 22 | # Set the container image name to the Github repository name. (MythicAgents/apfell) 23 | AGENT_IMAGE_NAME: ${{ github.repository }} 24 | 25 | # Description label for the package in Github 26 | IMAGE_DESCRIPTION: ${{ github.repository }} container for use with Mythic 27 | 28 | # Source URL for the package in Github. This links the Github repository packages list 29 | # to this container image 30 | IMAGE_SOURCE: ${{ github.server_url }}/${{ github.repository }} 31 | 32 | # License for the container image 33 | IMAGE_LICENSE: BSD-3-Clause 34 | 35 | # Set the container image version to the Github release tag 36 | VERSION: ${{ github.ref_name }} 37 | #VERSION: ${{ github.event.head_commit.message }} 38 | 39 | RELEASE_BRANCH: main 40 | 41 | jobs: 42 | # Builds the base container image and pushes it to the container registry 43 | agent_build: 44 | runs-on: ubuntu-latest 45 | permissions: 46 | contents: write 47 | packages: write 48 | steps: 49 | - name: Checkout the repository 50 | uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout 51 | - name: Log in to the container registry 52 | uses: docker/login-action@v3 # ref: https://github.com/marketplace/actions/docker-login 53 | with: 54 | registry: ${{ env.REGISTRY }} 55 | username: ${{ github.actor }} 56 | password: ${{ secrets.GITHUB_TOKEN }} 57 | - name: Set up QEMU 58 | uses: docker/setup-qemu-action@v2 59 | with: 60 | platforms: 'arm64,arm' 61 | - name: Set up Docker Buildx 62 | id: buildx 63 | uses: docker/setup-buildx-action@v2 64 | # the following are unique to this job 65 | - name: Lowercase the server container image name 66 | run: echo "AGENT_IMAGE_NAME=${AGENT_IMAGE_NAME,,}" >> ${GITHUB_ENV} 67 | - name: Build and push the server container image 68 | uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images 69 | with: 70 | context: Payload_Type/nemesis 71 | file: Payload_Type/nemesis/.docker/Dockerfile 72 | tags: | 73 | ${{ env.REGISTRY }}/${{ env.AGENT_IMAGE_NAME }}:${{ env.VERSION }} 74 | ${{ env.REGISTRY }}/${{ env.AGENT_IMAGE_NAME }}:latest 75 | push: ${{ github.ref_type == 'tag' }} 76 | # These container metadata labels allow configuring the package in Github 77 | # packages. The source will link the package to this Github repository 78 | labels: | 79 | org.opencontainers.image.source=${{ env.IMAGE_SOURCE }} 80 | org.opencontainers.image.description=${{ env.IMAGE_DESCRIPTION }} 81 | org.opencontainers.image.licenses=${{ env.IMAGE_LICENSE }} 82 | platforms: linux/amd64,linux/arm64 83 | 84 | update_files: 85 | runs-on: ubuntu-latest 86 | needs: 87 | - agent_build 88 | permissions: 89 | contents: write 90 | packages: write 91 | 92 | steps: 93 | # Pull in the repository code 94 | - name: Checkout the repository 95 | uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout 96 | 97 | # update names to lowercase 98 | - name: Lowercase the container image name 99 | run: echo "AGENT_IMAGE_NAME=${AGENT_IMAGE_NAME,,}" >> ${GITHUB_ENV} 100 | 101 | # The Dockerfile which Mythic uses to pull in the base container image needs to be 102 | # updated to reference the newly built container image 103 | - name: Fix the server Dockerfile reference to reference the new release tag 104 | working-directory: Payload_Type/nemesis 105 | run: | 106 | sed -i "s|^FROM ghcr\.io.*$|FROM ${REGISTRY}/${AGENT_IMAGE_NAME}:${VERSION}|" Dockerfile 107 | 108 | - name: Update package.json version 109 | uses: jossef/action-set-json-field@v2.1 110 | with: 111 | file: config.json 112 | field: remote_images.nemesis 113 | value: ${{env.REGISTRY}}/${{env.AGENT_IMAGE_NAME}}:${{env.VERSION}} 114 | 115 | # Push the changes to the Dockerfile 116 | - name: Push the updated base Dockerfile image reference changes 117 | if: ${{ github.ref_type == 'tag' }} 118 | uses: EndBug/add-and-commit@v9 # ref: https://github.com/marketplace/actions/add-commit 119 | with: 120 | # Only add the Dockerfile changes. Nothing else should have been modified 121 | add: "['Payload_Type/nemesis/Dockerfile', 'config.json']" 122 | # Use the Github actions bot for the commit author 123 | default_author: github_actions 124 | committer_email: github-actions[bot]@users.noreply.github.com 125 | 126 | # Set the commit message 127 | message: "Bump Dockerfile tag to match release '${{ env.VERSION }}'" 128 | 129 | # Overwrite the current git tag with the new changes 130 | tag: '${{ env.VERSION }} --force' 131 | 132 | # Push the new changes with the tag overwriting the current one 133 | tag_push: '--force' 134 | 135 | # Push the commits to the branch marked as the release branch 136 | push: origin HEAD:${{ env.RELEASE_BRANCH }} --set-upstream 137 | 138 | # Have the workflow fail in case there are pathspec issues 139 | pathspec_error_handling: exitImmediately 140 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .idea/ 6 | .DS_Store 7 | rabbitmq_config.json 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # celery beat schedule file 97 | celerybeat-schedule 98 | 99 | # SageMath parsed files 100 | *.sage.py 101 | 102 | # Environments 103 | .env 104 | .venv 105 | env/ 106 | venv/ 107 | ENV/ 108 | env.bak/ 109 | venv.bak/ 110 | 111 | # Spyder project settings 112 | .spyderproject 113 | .spyproject 114 | 115 | # Rope project settings 116 | .ropeproject 117 | 118 | # mkdocs documentation 119 | /site 120 | 121 | # mypy 122 | .mypy_cache/ 123 | .dmypy.json 124 | dmypy.json 125 | 126 | # Pyre type checker 127 | .pyre/ 128 | -------------------------------------------------------------------------------- /C2_Profiles/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MythicAgents/nemesis/242823dcb93473f8189ab4404618e13f3feaccc9/C2_Profiles/.keep -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2024, its-a-feature 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of nemesis, mythic, nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Payload_Type/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MythicAgents/nemesis/242823dcb93473f8189ab4404618e13f3feaccc9/Payload_Type/__init__.py -------------------------------------------------------------------------------- /Payload_Type/nemesis/.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim-bookworm as builder 2 | 3 | COPY [".docker/requirements.txt", "requirements.txt"] 4 | RUN apt-get -y update && \ 5 | apt-get -y upgrade && \ 6 | apt-get install --no-install-recommends \ 7 | software-properties-common apt-utils make build-essential libssl-dev zlib1g-dev libbz2-dev \ 8 | xz-utils tk-dev libffi-dev liblzma-dev libsqlite3-dev protobuf-compiler \ 9 | binutils-aarch64-linux-gnu libc-dev-arm64-cross -y 10 | RUN python3 -m pip wheel --wheel-dir /wheels -r requirements.txt 11 | 12 | FROM python:3.11-slim-bookworm 13 | 14 | COPY --from=builder /wheels /wheels 15 | 16 | RUN pip install --no-cache /wheels/* 17 | 18 | WORKDIR /Mythic/ 19 | 20 | COPY [".", "."] 21 | 22 | CMD ["python3", "main.py"] -------------------------------------------------------------------------------- /Payload_Type/nemesis/.docker/requirements.txt: -------------------------------------------------------------------------------- 1 | mythic-container==0.5.9 2 | requests 3 | gql -------------------------------------------------------------------------------- /Payload_Type/nemesis/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim-bookworm as builder 2 | 3 | COPY [".docker/requirements.txt", "requirements.txt"] 4 | RUN apt-get -y update && \ 5 | apt-get -y upgrade && \ 6 | apt-get install --no-install-recommends \ 7 | software-properties-common apt-utils make build-essential libssl-dev zlib1g-dev libbz2-dev \ 8 | xz-utils tk-dev libffi-dev liblzma-dev libsqlite3-dev protobuf-compiler \ 9 | binutils-aarch64-linux-gnu libc-dev-arm64-cross -y 10 | RUN python3 -m pip wheel --wheel-dir /wheels -r requirements.txt 11 | 12 | FROM python:3.11-slim-bookworm 13 | 14 | COPY --from=builder /wheels /wheels 15 | 16 | RUN pip install --no-cache /wheels/* 17 | 18 | WORKDIR /Mythic/ 19 | 20 | COPY [".", "."] 21 | 22 | CMD ["python3", "main.py"] -------------------------------------------------------------------------------- /Payload_Type/nemesis/main.py: -------------------------------------------------------------------------------- 1 | import mythic_container 2 | import asyncio 3 | # import the nemesis agent 4 | import nemesis 5 | 6 | mythic_container.mythic_service.start_and_run_forever() 7 | -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/NemesisRequests/NemesisAPI.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from nemesis.NemesisRequests.NemesisAPIClasses import * 3 | from mythic_container.MythicRPC import * 4 | from gql import gql 5 | from datetime import datetime, timedelta 6 | 7 | NEMESIS_USERNAME = "NEMESIS_USERNAME" 8 | NEMESIS_PASSWORD = "NEMESIS_PASSWORD" 9 | 10 | 11 | def check_valid_values(username, password, url) -> bool: 12 | if username == "" or username is None: 13 | logger.error("missing username") 14 | return False 15 | if password == "" or password is None: 16 | logger.error("missing password") 17 | return False 18 | if url == "" or url is None: 19 | logger.error("missing url") 20 | return False 21 | return True 22 | 23 | 24 | def convert_timestamp(timestamp, days_to_add=0): 25 | """ 26 | Strips off the microseconds from a timestamp and reformats to our unified format. 27 | 28 | **Parameters** 29 | 30 | ``timestamp`` 31 | The timestamp string to reformat. 32 | 33 | **Returns** 34 | 35 | A reformatted timestamp string. 36 | """ 37 | 38 | dt = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%fZ") 39 | 40 | if days_to_add != 0: 41 | dt = dt + timedelta(days=days_to_add) 42 | 43 | return dt.strftime("%Y-%m-%dT%H:%M:%S.000Z") 44 | 45 | 46 | async def query_graphql(taskData: PTTaskMessageAllData, query: gql, uri: str = '/hasura/v1/graphql', 47 | variable_values: dict = None) -> (int, dict): 48 | username = None 49 | password = None 50 | url = None 51 | for buildParam in taskData.BuildParameters: 52 | if buildParam.Name == "URL": 53 | url = buildParam.Value 54 | if NEMESIS_USERNAME in taskData.Secrets: 55 | username = taskData.Secrets[NEMESIS_USERNAME] 56 | if NEMESIS_PASSWORD in taskData.Secrets: 57 | password = taskData.Secrets[NEMESIS_PASSWORD] 58 | if not check_valid_values(username, password, url): 59 | return 500, f"Missing {NEMESIS_USERNAME} or {NEMESIS_PASSWORD} in User settings or missing Nemesis URL" 60 | try: 61 | credentials = Credentials(username=username, password=password) 62 | client = NemesisClient(url=url.rstrip("/") + uri, credentials=credentials) 63 | response = await client.graphql_query(query=query, variable_values=variable_values) 64 | logger.info(f"Nemesis Query: {uri}") 65 | if response is not None: 66 | return 200, response 67 | else: 68 | return 500, client.last_error 69 | except Exception as e: 70 | logger.exception(f"[-] Failed to query Nemesis: \n{e}\n") 71 | raise Exception(f"[-] Failed to query Nemesis: \n{e}\n") 72 | 73 | 74 | async def post_data_api(taskData: PTTaskMessageAllData, data: dict) -> (int, dict): 75 | username = None 76 | password = None 77 | url = None 78 | for buildParam in taskData.BuildParameters: 79 | if buildParam.Name == "URL": 80 | url = buildParam.Value 81 | if NEMESIS_USERNAME in taskData.Secrets: 82 | username = taskData.Secrets[NEMESIS_USERNAME] 83 | if NEMESIS_PASSWORD in taskData.Secrets: 84 | password = taskData.Secrets[NEMESIS_PASSWORD] 85 | if not check_valid_values(username, password, url): 86 | return 500, f"Missing {NEMESIS_USERNAME} or {NEMESIS_PASSWORD} in User settings or missing Nemesis URL" 87 | try: 88 | credentials = Credentials(username=username, password=password) 89 | client = NemesisClient(url=url.rstrip("/"), credentials=credentials, graphql=False) 90 | response = await client.nemesis_post_data(data=data) 91 | if response is not None: 92 | return 200, response 93 | else: 94 | return 500, client.last_error 95 | except Exception as e: 96 | logger.exception(f"[-] Failed to post data to Nemesis: \n{e}\n") 97 | raise Exception(f"[-] Failed to post data to Nemesis: \n{e}\n") 98 | 99 | 100 | async def post_file_api(taskData: PTTaskMessageAllData, file_bytes: bytes) -> (int, str): 101 | username = None 102 | password = None 103 | url = None 104 | for buildParam in taskData.BuildParameters: 105 | if buildParam.Name == "URL": 106 | url = buildParam.Value 107 | if NEMESIS_USERNAME in taskData.Secrets: 108 | username = taskData.Secrets[NEMESIS_USERNAME] 109 | if NEMESIS_PASSWORD in taskData.Secrets: 110 | password = taskData.Secrets[NEMESIS_PASSWORD] 111 | if not check_valid_values(username, password, url): 112 | return 500, f"Missing {NEMESIS_USERNAME} or {NEMESIS_PASSWORD} in User settings or missing Nemesis URL" 113 | try: 114 | credentials = Credentials(username=username, password=password) 115 | client = NemesisClient(url=url.rstrip("/"), credentials=credentials, graphql=False) 116 | response = await client.nemesis_post_file(file_bytes=file_bytes) 117 | if response is not None: 118 | return 200, response 119 | else: 120 | return 500, client.last_error 121 | except Exception as e: 122 | logger.exception(f"[-] Failed to post file to Nemesis: \n{e}\n") 123 | raise Exception(f"[-] Failed to post file to Nemesis: \n{e}\n") 124 | 125 | 126 | async def process_standard_response(response_code: int, response_data: any, 127 | taskData: PTTaskMessageAllData, response: PTTaskCreateTaskingMessageResponse) -> \ 128 | PTTaskCreateTaskingMessageResponse: 129 | if response_code == 200: 130 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 131 | TaskID=taskData.Task.ID, 132 | Response=json.dumps(response_data).encode("UTF8"), 133 | )) 134 | response.Success = True 135 | else: 136 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 137 | TaskID=taskData.Task.ID, 138 | Response=f"{response_data}".encode("UTF8"), 139 | )) 140 | response.TaskStatus = "Error: Nemesis Query Error" 141 | response.Success = False 142 | return response 143 | -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/NemesisRequests/NemesisAPIClasses.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import hashlib 3 | import base64 4 | import requests 5 | from requests.auth import HTTPBasicAuth 6 | import datetime 7 | import asyncio 8 | from gql import Client, gql 9 | from gql.transport.aiohttp import AIOHTTPTransport 10 | from gql.transport.exceptions import TransportQueryError 11 | from graphql.error.graphql_error import GraphQLError 12 | from mythic_container.logging import logger 13 | 14 | from typing import Optional 15 | 16 | 17 | 18 | 19 | class Credentials(object): 20 | def __init__(self, username: str, password: str) -> None: 21 | self.username = username 22 | self.password = password 23 | 24 | 25 | class NemesisClient(object): 26 | def __init__(self, url: str, credentials: Credentials, graphql: bool = True) -> None: 27 | self._url = url 28 | self._credentials = credentials 29 | self.headers = { 30 | "User-Agent": f"Nemesis_Agent/1.0", 31 | "Authorization": "Basic " + base64.b64encode(f"{self._credentials.username}:{self._credentials.password}".encode()).decode("ascii"), 32 | "Content-Type": "application/json" 33 | } 34 | self._graphql = graphql 35 | if graphql: 36 | self.transport = AIOHTTPTransport(url=self._url, timeout=10, headers=self.headers) 37 | self.client = Client(transport=self.transport, fetch_schema_from_transport=False, ) 38 | self.session = None 39 | self.last_error = "" 40 | 41 | def __del__(self): 42 | if self._graphql: 43 | asyncio.create_task(self.client.close_async()) 44 | 45 | async def graphql_query(self, query: gql, variable_values: Optional[dict] = None) -> dict[str, any]: 46 | if self.session is None: 47 | self.session = await self.client.connect_async(reconnecting=True) 48 | # Perform the request with the signed and expected headers 49 | try: 50 | result = await self.session.execute(query, variable_values=variable_values) 51 | self.last_error = "" 52 | return result 53 | except TimeoutError: 54 | logger.error( 55 | "Timeout occurred while trying to connect to Nemesis at %s", 56 | self._url 57 | ) 58 | self.last_error = "timeout trying to connect to Nemesis" 59 | return None 60 | except TransportQueryError as e: 61 | logger.exception("Error encountered while fetching GraphQL schema: %s", e) 62 | payload = e.errors[0] 63 | self.last_error = f"GraphQL error: {payload['message']}" 64 | if "extensions" in payload: 65 | if "code" in payload["extensions"]: 66 | if payload["extensions"]["code"] == "access-denied": 67 | logger.error( 68 | "Access denied for the provided Nemesis credentials! Check if it is valid, update your configuration, and restart") 69 | self.last_error = f"Access denied for credentials" 70 | if payload["extensions"]["code"] == "postgres-error": 71 | logger.error( 72 | "Nemesis's database rejected the query!") 73 | self.last_error = f"Database error" 74 | return None 75 | except GraphQLError as e: 76 | logger.exception("Error with GraphQL query: %s", e) 77 | self.last_error = f"Graphql Error: {e}" 78 | return None 79 | except Exception as e: 80 | logger.exception(e) 81 | self.last_error = f"Unknown Error: {e}" 82 | return None 83 | 84 | async def nemesis_post_data(self, data): 85 | """ 86 | Takes a json blob and POSTs it to the NEMESIS /data API endpoint. 87 | 88 | **Parameters** 89 | 90 | ``data`` 91 | JSON formatted blob to post. 92 | 93 | **Returns** 94 | 95 | True if the request was successful, False otherwise. 96 | """ 97 | try: 98 | basic = HTTPBasicAuth(self._credentials.username, self._credentials.password) 99 | logger.info(f"Posting Data: {data}") 100 | r = requests.post(f"{self._url}/api/data", auth=basic, json=data) 101 | if r.status_code != 200: 102 | raise Exception(r.text) 103 | else: 104 | return r.json() 105 | except Exception as e: 106 | logger.error(f"[nemesis_post_data] Error : {e}") 107 | raise e 108 | 109 | async def nemesis_post_file(self, file_bytes): 110 | """ 111 | Takes a series of raw file bytes and POSTs it to the NEMESIS /file API endpoint. 112 | 113 | **Parameters** 114 | 115 | ``file_bytes`` 116 | Bytes of the file we're uploading. 117 | 118 | **Returns** 119 | 120 | A new UUID string returned by the Nemesis API. 121 | """ 122 | try: 123 | basic = HTTPBasicAuth(self._credentials.username, self._credentials.password) 124 | logger.info(f"Nemesis post to {self._url}/api/file") 125 | r = requests.request("POST", f"{self._url}/api/file", auth=basic, data=file_bytes, headers={"Content-Type": "application/octet-stream"}) 126 | if r.status_code != 200: 127 | raise Exception(r.text) 128 | else: 129 | json_result = r.json() 130 | if "object_id" in json_result: 131 | return json_result["object_id"] 132 | else: 133 | raise Exception("[nemesis_post_file] Error retrieving 'object_id' field from result") 134 | except Exception as e: 135 | raise e -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/NemesisRequests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MythicAgents/nemesis/242823dcb93473f8189ab4404618e13f3feaccc9/Payload_Type/nemesis/nemesis/NemesisRequests/__init__.py -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/__init__.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os.path 3 | from pathlib import Path 4 | from importlib import import_module, invalidate_caches 5 | import sys 6 | # Get file paths of all modules. 7 | 8 | currentPath = Path(__file__) 9 | searchPath = currentPath.parent / "agent_functions" / "*.py" 10 | modules = glob.glob(f"{searchPath}") 11 | invalidate_caches() 12 | for x in modules: 13 | if not x.endswith("__init__.py") and x[-3:] == ".py": 14 | module = import_module(f"{__name__}.agent_functions." + Path(x).stem) 15 | for el in dir(module): 16 | if "__" not in el: 17 | globals()[el] = getattr(module, el) 18 | 19 | 20 | sys.path.append(os.path.abspath(currentPath.name)) 21 | -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/agent_functions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MythicAgents/nemesis/242823dcb93473f8189ab4404618e13f3feaccc9/Payload_Type/nemesis/nemesis/agent_functions/__init__.py -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/agent_functions/builder.py: -------------------------------------------------------------------------------- 1 | from mythic_container.PayloadBuilder import * 2 | from mythic_container.MythicCommandBase import * 3 | from mythic_container.MythicRPC import * 4 | 5 | 6 | class Nemesis(PayloadType): 7 | name = "nemesis" 8 | file_extension = "" 9 | author = "@its_a_feature_" 10 | supported_os = [ 11 | SupportedOS("nemesis") 12 | ] 13 | wrapper = False 14 | wrapped_payloads = [] 15 | note = """ 16 | This payload communicates with an existing Nemesis instance. In your settings, add your Nemesis Username and Password as a secret with the keys "NEMESIS_USERNAME" and "NEMESIS_PASSWORD". 17 | """ 18 | supports_dynamic_loading = False 19 | mythic_encrypts = True 20 | translation_container = None 21 | agent_type = "service" 22 | agent_path = pathlib.Path(".") / "nemesis" 23 | agent_icon_path = agent_path / "agent_functions" / "nemesis.svg" 24 | agent_code_path = agent_path / "agent_code" 25 | build_parameters = [ 26 | BuildParameter(name="URL", 27 | description="Nemesis URL", 28 | parameter_type=BuildParameterType.String, 29 | default_value="https://127.0.0.1:8080"), 30 | ] 31 | c2_profiles = [] 32 | 33 | async def build(self) -> BuildResponse: 34 | # this function gets called to create an instance of your payload 35 | resp = BuildResponse(status=BuildStatus.Success) 36 | ip = "127.0.0.1" 37 | create_callback = await SendMythicRPCCallbackCreate(MythicRPCCallbackCreateMessage( 38 | PayloadUUID=self.uuid, 39 | C2ProfileName="", 40 | User="Nemesis", 41 | Host="Nemesis", 42 | Ip=ip, 43 | IntegrityLevel=3, 44 | )) 45 | if not create_callback.Success: 46 | logger.info(create_callback.Error) 47 | else: 48 | logger.info(create_callback.CallbackUUID) 49 | return resp 50 | -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/agent_functions/chromium.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from nemesis.NemesisRequests import NemesisAPI 4 | from gql import gql 5 | 6 | 7 | class ChromiumArguments(TaskArguments): 8 | 9 | def __init__(self, command_line, **kwargs): 10 | super().__init__(command_line, **kwargs) 11 | self.args = [ 12 | ] 13 | 14 | async def parse_arguments(self): 15 | pass 16 | 17 | async def parse_dictionary(self, dictionary_arguments): 18 | pass 19 | 20 | 21 | class Chromium(CommandBase): 22 | cmd = "chromium" 23 | needs_admin = False 24 | help_cmd = "chromium" 25 | description = "Get Chromium information from Nemesis" 26 | version = 2 27 | author = "@its_a_feature_" 28 | argument_class = ChromiumArguments 29 | supported_ui_features = ["nemesis:chromium"] 30 | browser_script = BrowserScript(script_name="chromium", author="@its_a_feature_") 31 | attackmapping = [] 32 | completion_functions = { 33 | } 34 | 35 | async def create_go_tasking(self, 36 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 37 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 38 | TaskID=taskData.Task.ID, 39 | Success=False, 40 | Completed=True, 41 | DisplayParams=f"" 42 | ) 43 | chromium_get_query = gql( 44 | """ 45 | query NemesisChromium($project_id: String!) { 46 | nemesis_chromium_logins(where: {project_id: {_eq: $project_id}}) { 47 | agent_id 48 | browser 49 | password_value_dec 50 | signon_realm 51 | source 52 | user_data_directory 53 | username 54 | username_value 55 | project_id 56 | } 57 | nemesis_chromium_history(order_by: {last_visit_time: desc}, where: {project_id: {_eq: $project_id}}, limit: 50) { 58 | agent_id 59 | browser 60 | last_visit_time 61 | typed_count 62 | url 63 | username 64 | visit_count 65 | title 66 | source 67 | } 68 | nemesis_chromium_downloads(where: {project_id: {_eq: $project_id}}, order_by: {start_time: desc}) { 69 | browser 70 | download_path 71 | source 72 | timestamp 73 | total_bytes 74 | username 75 | url 76 | } 77 | } 78 | """ 79 | ) 80 | try: 81 | response_code, response_data = await NemesisAPI.query_graphql(taskData, query=chromium_get_query, 82 | variable_values={ 83 | "project_id": taskData.Callback.OperationName 84 | }) 85 | return await NemesisAPI.process_standard_response(response_code=response_code, 86 | response_data=response_data, 87 | taskData=taskData, 88 | response=response) 89 | except Exception as e: 90 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 91 | TaskID=taskData.Task.ID, 92 | Response=f"{e}".encode("UTF8"), 93 | )) 94 | response.TaskStatus = "Error: Nemesis Access Error" 95 | response.Success = False 96 | return response 97 | 98 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 99 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 100 | return resp 101 | -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/agent_functions/credentials.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from nemesis.NemesisRequests import NemesisAPI 4 | from gql import gql 5 | 6 | 7 | class CredentialsArguments(TaskArguments): 8 | 9 | def __init__(self, command_line, **kwargs): 10 | super().__init__(command_line, **kwargs) 11 | self.args = [ 12 | ] 13 | 14 | async def parse_arguments(self): 15 | #self.load_args_from_json_string(self.command_line) 16 | pass 17 | 18 | async def parse_dictionary(self, dictionary_arguments): 19 | #self.load_args_from_dictionary(dictionary=dictionary_arguments) 20 | pass 21 | 22 | 23 | class Credentials(CommandBase): 24 | cmd = "credentials" 25 | needs_admin = False 26 | help_cmd = "credentials" 27 | description = "Get credential information from Nemesis" 28 | version = 2 29 | author = "@its_a_feature_" 30 | argument_class = CredentialsArguments 31 | supported_ui_features = ["nemesis:credentials"] 32 | browser_script = BrowserScript(script_name="credentials", author="@its_a_feature_") 33 | attackmapping = [] 34 | completion_functions = { 35 | } 36 | 37 | async def create_go_tasking(self, 38 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 39 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 40 | TaskID=taskData.Task.ID, 41 | Success=False, 42 | Completed=True, 43 | DisplayParams=f"" 44 | ) 45 | triage_get_query = gql( 46 | """ 47 | query NemesisAuthenticationData($project_id: String!) { 48 | nemesis_authentication_data(where: {project_id: {_eq: $project_id}}) { 49 | data 50 | username 51 | uri 52 | unique_db_id 53 | type 54 | timestamp 55 | source 56 | project_id 57 | originating_object_id 58 | notes 59 | is_file 60 | expiration 61 | agent_id 62 | } 63 | } 64 | """ 65 | ) 66 | try: 67 | response_code, response_data = await NemesisAPI.query_graphql(taskData, query=triage_get_query, 68 | variable_values={ 69 | "project_id": taskData.Callback.OperationName 70 | }) 71 | return await NemesisAPI.process_standard_response(response_code=response_code, 72 | response_data=response_data, 73 | taskData=taskData, 74 | response=response) 75 | except Exception as e: 76 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 77 | TaskID=taskData.Task.ID, 78 | Response=f"{e}".encode("UTF8"), 79 | )) 80 | response.TaskStatus = "Error: Nemesis Access Error" 81 | response.Success = False 82 | return response 83 | 84 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 85 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 86 | return resp 87 | -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/agent_functions/hashes.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from nemesis.NemesisRequests import NemesisAPI 4 | from gql import gql 5 | 6 | 7 | class HashesArguments(TaskArguments): 8 | 9 | def __init__(self, command_line, **kwargs): 10 | super().__init__(command_line, **kwargs) 11 | self.args = [ 12 | ] 13 | 14 | async def parse_arguments(self): 15 | self.load_args_from_json_string(self.command_line) 16 | 17 | async def parse_dictionary(self, dictionary_arguments): 18 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 19 | 20 | 21 | class Hashes(CommandBase): 22 | cmd = "hashes" 23 | needs_admin = False 24 | help_cmd = "hashes" 25 | description = "Get extracted hash information from Nemesis" 26 | version = 2 27 | author = "@its_a_feature_" 28 | argument_class = HashesArguments 29 | supported_ui_features = ["nemesis:hashes"] 30 | browser_script = BrowserScript(script_name="hashes", author="@its_a_feature_") 31 | attackmapping = [] 32 | completion_functions = { 33 | } 34 | 35 | async def create_go_tasking(self, 36 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 37 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 38 | TaskID=taskData.Task.ID, 39 | Success=False, 40 | Completed=True, 41 | DisplayParams=f"" 42 | ) 43 | triage_get_query = gql( 44 | """ 45 | query NemesisHashes($project_id: String!) { 46 | nemesis_extracted_hashes(where: {project_id: {_eq: $project_id}}) { 47 | agent_id 48 | checked_against_top_passwords 49 | cracker_cracked_time 50 | cracker_submission_time 51 | expiration 52 | hash_type 53 | hash_value 54 | hash_value_md5_hash 55 | hashcat_formatted_value 56 | is_cracked 57 | is_submitted_to_cracker 58 | jtr_formatted_value 59 | originating_object_id 60 | plaintext_value 61 | project_id 62 | source 63 | timestamp 64 | unique_db_id 65 | } 66 | } 67 | """ 68 | ) 69 | try: 70 | response_code, response_data = await NemesisAPI.query_graphql(taskData, query=triage_get_query, 71 | variable_values={ 72 | "project_id": taskData.Callback.OperationName 73 | }) 74 | return await NemesisAPI.process_standard_response(response_code=response_code, 75 | response_data=response_data, 76 | taskData=taskData, 77 | response=response) 78 | except Exception as e: 79 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 80 | TaskID=taskData.Task.ID, 81 | Response=f"{e}".encode("UTF8"), 82 | )) 83 | response.TaskStatus = "Error: Nemesis Access Error" 84 | response.Success = False 85 | return response 86 | 87 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 88 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 89 | return resp 90 | -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/agent_functions/nemesis.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/agent_functions/triage.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from nemesis.NemesisRequests import NemesisAPI 4 | from gql import gql 5 | 6 | 7 | class TriageArguments(TaskArguments): 8 | 9 | def __init__(self, command_line, **kwargs): 10 | super().__init__(command_line, **kwargs) 11 | self.args = [ 12 | CommandParameter( 13 | name="value", 14 | default_value="both", 15 | type=ParameterType.ChooseOne, 16 | choices=["useful", "notuseful", "both"], 17 | parameter_group_info=[ 18 | ParameterGroupInfo(required=False) 19 | ] 20 | ) 21 | ] 22 | 23 | async def parse_arguments(self): 24 | self.load_args_from_json_string(self.command_line) 25 | if self.get_arg("value") is None: 26 | if self.command_line == "": 27 | self.set_arg("value", "both") 28 | else: 29 | self.set_arg("value", self.command_line) 30 | 31 | async def parse_dictionary(self, dictionary_arguments): 32 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 33 | 34 | 35 | class Triage(CommandBase): 36 | cmd = "triage" 37 | needs_admin = False 38 | help_cmd = "triage" 39 | description = "Get information about which files have already been triaged in nemesis and marked useful or not" 40 | version = 2 41 | author = "@its_a_feature_" 42 | argument_class = TriageArguments 43 | supported_ui_features = ["nemesis:triage"] 44 | browser_script = BrowserScript(script_name="triage", author="@its_a_feature_") 45 | attackmapping = [] 46 | completion_functions = { 47 | } 48 | 49 | async def create_go_tasking(self, 50 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 51 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 52 | TaskID=taskData.Task.ID, 53 | Success=False, 54 | Completed=True, 55 | DisplayParams=f"" 56 | ) 57 | triage_get_query = gql( 58 | """ 59 | query NemesisTriage($project_id: String!) { 60 | nemesis_triage(where: {file_data_enriched: {project_id: {_eq: $project_id}}}) { 61 | value 62 | operator 63 | expiration 64 | file_data_enriched { 65 | project_id 66 | magic_type 67 | name 68 | nemesis_file_type 69 | tags 70 | } 71 | } 72 | } 73 | """ 74 | ) 75 | try: 76 | response_code, response_data = await NemesisAPI.query_graphql(taskData, query=triage_get_query, 77 | variable_values={ 78 | "project_id": taskData.Callback.OperationName 79 | }) 80 | if response_code == 500: 81 | return await NemesisAPI.process_standard_response(response_code=response_code, 82 | response_data=response_data, 83 | taskData=taskData, 84 | response=response) 85 | results = [x for x in response_data["nemesis_triage"] if taskData.args.get_arg("value") == "both" or taskData.args.get_arg("value") == x["value"]] 86 | return await NemesisAPI.process_standard_response(response_code=response_code, 87 | response_data=results, 88 | taskData=taskData, 89 | response=response) 90 | except Exception as e: 91 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 92 | TaskID=taskData.Task.ID, 93 | Response=f"{e}".encode("UTF8"), 94 | )) 95 | response.TaskStatus = "Error: Nemesis Access Error" 96 | response.Success = False 97 | return response 98 | 99 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 100 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 101 | return resp 102 | -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/agent_functions/upload.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from nemesis.NemesisRequests import NemesisAPI 4 | from gql import gql 5 | 6 | 7 | class UploadArguments(TaskArguments): 8 | 9 | def __init__(self, command_line, **kwargs): 10 | super().__init__(command_line, **kwargs) 11 | self.args = [ 12 | CommandParameter( 13 | name="file", 14 | type=ParameterType.File, 15 | parameter_group_info=[ParameterGroupInfo( 16 | required=True, 17 | ui_position=1, 18 | group_name="Manually Upload New File" 19 | )] 20 | ), 21 | CommandParameter( 22 | name="filename", 23 | type=ParameterType.ChooseOne, 24 | dynamic_query_function=self.get_files, 25 | parameter_group_info=[ParameterGroupInfo( 26 | required=True, 27 | ui_position=1, 28 | group_name="Select Mythic File to Upload" 29 | )] 30 | ), 31 | CommandParameter( 32 | name="remote_path", 33 | type=ParameterType.String, 34 | description="The absolute remote path on the target host where this file came from. If you're uploading a file that Mythic already has a path for, you can leave this blank and that path will automatically get used. Otherwise, you should specify the path. Certain files (DPAPI keys, Chrome data, etc) relies on the full paths within Nemesis.", 35 | default_value="", 36 | parameter_group_info=[ 37 | ParameterGroupInfo( 38 | required=False, 39 | ui_position=2, 40 | group_name="Manually Upload New File" 41 | ), 42 | ParameterGroupInfo( 43 | required=False, 44 | ui_position=2, 45 | group_name="Select Mythic File to Upload" 46 | ) 47 | ] 48 | ), 49 | #CommandParameter( 50 | # name="domain_backup_key", 51 | # type=ParameterType.Boolean, 52 | # description="Specify that this is a domain backup key so that it can be processed and used to decrypt DPAPI Blobs", 53 | # default_value=False, 54 | # parameter_group_info=[ 55 | # ParameterGroupInfo( 56 | # required=False, 57 | # ui_position=2, 58 | # group_name="Manually Upload New File" 59 | # ), 60 | # ParameterGroupInfo( 61 | # required=False, 62 | # ui_position=2, 63 | # group_name="Select Mythic File to Upload" 64 | # ) 65 | # ] 66 | #) 67 | ] 68 | 69 | async def parse_arguments(self): 70 | self.load_args_from_json_string(self.command_line) 71 | 72 | async def parse_dictionary(self, dictionary_arguments): 73 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 74 | 75 | async def get_files(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: 76 | response = PTRPCDynamicQueryFunctionMessageResponse() 77 | file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( 78 | CallbackID=callback.Callback, 79 | LimitByCallback=False, 80 | IsDownloadFromAgent=True, 81 | IsScreenshot=False, 82 | IsPayload=False, 83 | Filename="", 84 | )) 85 | if file_resp.Success: 86 | file_names = [] 87 | for f in file_resp.Files: 88 | if f.Filename not in file_names: 89 | file_names.append(f.Filename) 90 | response.Success = True 91 | response.Choices = file_names 92 | return response 93 | else: 94 | await SendMythicRPCOperationEventLogCreate(MythicRPCOperationEventLogCreateMessage( 95 | CallbackId=callback.Callback, 96 | Message=f"Failed to get files: {file_resp.Error}", 97 | MessageLevel="warning" 98 | )) 99 | response.Error = f"Failed to get files: {file_resp.Error}" 100 | return response 101 | 102 | 103 | class Upload(CommandBase): 104 | cmd = "upload" 105 | needs_admin = False 106 | help_cmd = "upload" 107 | description = "Upload a new file to Nemesis for processing" 108 | version = 2 109 | author = "@its_a_feature_" 110 | argument_class = UploadArguments 111 | supported_ui_features = ["nemesis:upload"] 112 | # browser_script = BrowserScript(script_name="hashes", author="@its_a_feature_") 113 | attackmapping = [] 114 | completion_functions = { 115 | } 116 | 117 | async def create_go_tasking(self, 118 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 119 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 120 | TaskID=taskData.Task.ID, 121 | Success=False, 122 | Completed=True, 123 | DisplayParams=f"" 124 | ) 125 | 126 | try: 127 | fileMetadata = None 128 | if taskData.args.get_parameter_group_name() == "Manually Upload New File": 129 | searchedFile = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( 130 | AgentFileID=taskData.args.get_arg("file") 131 | )) 132 | if not searchedFile.Success: 133 | raise Exception(searchedFile.Error) 134 | if len(searchedFile.Files) != 1: 135 | raise Exception("Failed to get file back from Mythic") 136 | fileMetadata = searchedFile.Files[0] 137 | 138 | else: 139 | searchedFile = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( 140 | TaskID=taskData.Task.ID, 141 | Filename=taskData.args.get_arg("filename"), 142 | LimitByCallback=False, 143 | MaxResults=1 144 | )) 145 | if not searchedFile.Success: 146 | raise Exception(searchedFile.Error) 147 | if len(searchedFile.Files) != 1: 148 | raise Exception("Failed to get file back from Mythic") 149 | fileMetadata = searchedFile.Files[0] 150 | fileContentsResp = await SendMythicRPCFileGetContent(MythicRPCFileGetContentMessage( 151 | AgentFileId=fileMetadata.AgentFileId 152 | )) 153 | if not fileContentsResp.Success: 154 | raise Exception(fileContentsResp.Error) 155 | # register file contents with Nemesis and get back file_id 156 | registerFileResponseCode, registerFileResponseData = await NemesisAPI.post_file_api(taskData=taskData, 157 | file_bytes=fileContentsResp.Content) 158 | if registerFileResponseCode != 200: 159 | return await NemesisAPI.process_standard_response(response_code=registerFileResponseCode, 160 | response_data=registerFileResponseData, 161 | taskData=taskData, 162 | response=response) 163 | # update file_id with additional metadata 164 | metadata = {} 165 | metadata["agent_id"] = taskData.Callback.AgentCallbackID 166 | metadata["agent_type"] = "mythic" 167 | metadata["automated"] = False 168 | metadata["data_type"] = "file_data" 169 | metadata["project"] = f"{taskData.Callback.OperationName}" 170 | metadata["timestamp"] = NemesisAPI.convert_timestamp(fileMetadata.Timestamp) 171 | metadata["expiration"] = NemesisAPI.convert_timestamp(fileMetadata.Timestamp, 90) 172 | file_data = {} 173 | if taskData.args.get_arg("remote_path") == "": 174 | file_data["path"] = fileMetadata.FullRemotePath 175 | else: 176 | file_data["path"] = taskData.args.get_arg("remote_path") 177 | if file_data["path"] == "": 178 | file_data["path"] = fileMetadata.Filename 179 | file_data["size"] = len(fileContentsResp.Content) 180 | file_data["object_id"] = registerFileResponseData 181 | response.DisplayParams = f"-remote_path {file_data['path']}" 182 | #if taskData.args.get_arg("domain_backup_key"): 183 | # metadata = { 184 | # "agent_id": taskData.Callback.OperatorUsername, 185 | # "data_type": "raw_data", 186 | # "agent_type": "submit_to_nemesis", 187 | # "automated": False, 188 | # "expiration": NemesisAPI.convert_timestamp(fileMetadata.Timestamp, 90), 189 | # "project": f"{taskData.Callback.OperationName}", 190 | # "timestamp": NemesisAPI.convert_timestamp(fileMetadata.Timestamp) 191 | # } 192 | # file_data = { 193 | # "tags": ["dpapi_domain_backupkey"], 194 | # "data": registerFileResponseData, 195 | # "is_file": True 196 | # } 197 | updateFileResponseCode, updateFileResponseData = await NemesisAPI.post_data_api(taskData=taskData, 198 | data={ 199 | "metadata": metadata, 200 | "data": [file_data] 201 | }) 202 | responseData = { 203 | "file_id": registerFileResponseData, 204 | "update_file": updateFileResponseData["object_id"] 205 | } 206 | 207 | return await NemesisAPI.process_standard_response(response_code=updateFileResponseCode, 208 | response_data=responseData, 209 | taskData=taskData, 210 | response=response) 211 | except Exception as e: 212 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 213 | TaskID=taskData.Task.ID, 214 | Response=f"{e}".encode("UTF8"), 215 | )) 216 | response.TaskStatus = "Error: Nemesis Access Error" 217 | response.Success = False 218 | return response 219 | 220 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 221 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 222 | return resp 223 | -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/browser_scripts/chromium.js: -------------------------------------------------------------------------------- 1 | function(task, responses){ 2 | if(task.status.includes("error")){ 3 | const combined = responses.reduce( (prev, cur) => { 4 | return prev + cur; 5 | }, ""); 6 | return {'plaintext': combined}; 7 | }else if(task.completed){ 8 | if(responses.length > 0){ 9 | let tables = []; 10 | try{ 11 | let allData = JSON.parse(responses[0]); 12 | let data = allData["nemesis_chromium_logins"]; 13 | let output_table = []; 14 | for(let i = 0; i < data.length; i++){ 15 | output_table.push({ 16 | "browser":{"plaintext": data[i]["browser"]}, 17 | "decrypted": {"plaintext": data[i]["password_value_dec"]}, 18 | "realm": {"plaintext": data[i]["signon_realm"] }, 19 | "user": {"plaintext": data[i]["username"]}, 20 | "username": {"plaintext": data[i]["username_value"]}, 21 | "actions": {"button": { 22 | "name": "Actions", 23 | "type": "menu", 24 | "value": [ 25 | { 26 | "name": "View All Data", 27 | "type": "dictionary", 28 | "value": data[i], 29 | "leftColumnTitle": "Key", 30 | "rightColumnTitle": "Value", 31 | "title": "Viewing Logon Data" 32 | }, 33 | ] 34 | }}, 35 | }); 36 | } 37 | tables.push( 38 | { 39 | "headers": [ 40 | {"plaintext": "browser", "type": "string", width: 70}, 41 | {"plaintext": "decrypted", "type": "string", fillWidth: true}, 42 | {"plaintext": "realm", "type": "string", fillWidth: true}, 43 | {"plaintext": "user", "type": "string", fillWidth: true}, 44 | {"plaintext": "username", "type": "string", fillWidth: true}, 45 | {"plaintext": "actions", "type": "button", "width": 70}, 46 | ], 47 | "rows": output_table, 48 | "title": "Login Data" 49 | } 50 | ); 51 | data = allData["nemesis_chromium_history"]; 52 | output_table = []; 53 | for(let i = 0; i < data.length; i++){ 54 | output_table.push({ 55 | "browser":{"plaintext": data[i]["browser"]}, 56 | "title": {"plaintext": data[i]["title"]}, 57 | "count": {"plaintext": data[i]["visit_count"] }, 58 | "last": {"plaintext": data[i]["last_visit_time"]}, 59 | "user": {"plaintext": data[i]["user"]}, 60 | "actions": {"button": { 61 | "name": "Actions", 62 | "type": "menu", 63 | "value": [ 64 | { 65 | "name": "View All Data", 66 | "type": "dictionary", 67 | "value": data[i], 68 | "leftColumnTitle": "Key", 69 | "rightColumnTitle": "Value", 70 | "title": "Viewing History Data" 71 | }, 72 | ] 73 | }}, 74 | }); 75 | } 76 | tables.push( 77 | { 78 | "headers": [ 79 | {"plaintext": "browser", "type": "string", width: 70}, 80 | {"plaintext": "title", "type": "string", fillWidth: true}, 81 | {"plaintext": "count", "type": "number", width: 70}, 82 | {"plaintext": "last", "type": "date", fillWidth: true}, 83 | {"plaintext": "user", "type": "string", fillWidth: true}, 84 | {"plaintext": "actions", "type": "button", "width": 70}, 85 | ], 86 | "rows": output_table, 87 | "title": "History" 88 | }); 89 | data = allData["nemesis_chromium_downloads"]; 90 | output_table = []; 91 | for(let i = 0; i < data.length; i++){ 92 | output_table.push({ 93 | "browser":{"plaintext": data[i]["browser"]}, 94 | "user": {"plaintext": data[i]["username"]}, 95 | "path": {"plaintext": data[i]["download_path"] }, 96 | "size": {"plaintext": data[i]["total_bytes"]}, 97 | "url": {"plaintext": data[i]["url"]}, 98 | "actions": {"button": { 99 | "name": "Actions", 100 | "type": "menu", 101 | "value": [ 102 | { 103 | "name": "View All Data", 104 | "type": "dictionary", 105 | "value": data[i], 106 | "leftColumnTitle": "Key", 107 | "rightColumnTitle": "Value", 108 | "title": "Viewing Download Data" 109 | }, 110 | ] 111 | }}, 112 | }); 113 | } 114 | tables.push( 115 | { 116 | "headers": [ 117 | {"plaintext": "browser", "type": "string", width: 70}, 118 | {"plaintext": "user", "type": "string", fillWidth: true}, 119 | {"plaintext": "path", "type": "string", fillWidth: true}, 120 | {"plaintext": "size", "type": "size", width: 70}, 121 | {"plaintext": "url", "type": "string", fillWidth: true}, 122 | {"plaintext": "actions", "type": "button", "width": 70}, 123 | ], 124 | "rows": output_table, 125 | "title": "Download History" 126 | } 127 | ) 128 | return {"table": tables} 129 | }catch(error){ 130 | console.log(error); 131 | const combined = responses.reduce( (prev, cur) => { 132 | return prev + cur; 133 | }, ""); 134 | return {'plaintext': combined}; 135 | } 136 | }else{ 137 | return {"plaintext": "No output from command"}; 138 | } 139 | }else{ 140 | return {"plaintext": "No data to display..."}; 141 | } 142 | } -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/browser_scripts/credentials.js: -------------------------------------------------------------------------------- 1 | function(task, responses){ 2 | if(task.status.includes("error")){ 3 | const combined = responses.reduce( (prev, cur) => { 4 | return prev + cur; 5 | }, ""); 6 | return {'plaintext': combined}; 7 | }else if(task.completed){ 8 | if(responses.length > 0){ 9 | try{ 10 | let data = JSON.parse(responses[0]); 11 | data = data["nemesis_authentication_data"]; 12 | let output_table = []; 13 | for(let i = 0; i < data.length; i++){ 14 | output_table.push({ 15 | "type":{"plaintext": data[i]["type"]}, 16 | "timestamp": {"plaintext": data[i]["timestamp"]}, 17 | "username": {"plaintext": data[i]["username"] }, 18 | "data": {"plaintext": data[i]["data"]}, 19 | "uri": {"plaintext": data[i]["uri"]}, 20 | "notes": {"plaintext": data[i]["notes"]}, 21 | "actions": {"button": { 22 | "name": "Actions", 23 | "type": "menu", 24 | "value": [ 25 | { 26 | "name": "View All Data", 27 | "type": "dictionary", 28 | "value": data[i], 29 | "leftColumnTitle": "Key", 30 | "rightColumnTitle": "Value", 31 | "title": "Viewing Credential Data" 32 | }, 33 | ] 34 | }}, 35 | }); 36 | } 37 | return { 38 | "table": [ 39 | { 40 | "headers": [ 41 | {"plaintext": "type", "type": "string", width: 100}, 42 | {"plaintext": "timestamp", "type": "date", width: 100}, 43 | {"plaintext": "username", "type": "string", fillWidth: true}, 44 | {"plaintext": "data", "type": "string", fillWidth: true}, 45 | {"plaintext": "uri", "type": "string", fillWidth: true}, 46 | {"plaintext": "notes", "type": "string", fillWidth: true}, 47 | {"plaintext": "actions", "type": "button", "width": 70}, 48 | ], 49 | "rows": output_table, 50 | "title": "Collected Credential Material" 51 | } 52 | ] 53 | } 54 | }catch(error){ 55 | console.log(error); 56 | const combined = responses.reduce( (prev, cur) => { 57 | return prev + cur; 58 | }, ""); 59 | return {'plaintext': combined}; 60 | } 61 | }else{ 62 | return {"plaintext": "No output from command"}; 63 | } 64 | }else{ 65 | return {"plaintext": "No data to display..."}; 66 | } 67 | } -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/browser_scripts/hashes.js: -------------------------------------------------------------------------------- 1 | function(task, responses){ 2 | if(task.status.includes("error")){ 3 | const combined = responses.reduce( (prev, cur) => { 4 | return prev + cur; 5 | }, ""); 6 | return {'plaintext': combined}; 7 | }else if(task.completed){ 8 | if(responses.length > 0){ 9 | try{ 10 | let data = JSON.parse(responses[0]); 11 | data = data["nemesis_extracted_hashes"]; 12 | let output_table = []; 13 | for(let i = 0; i < data.length; i++){ 14 | output_table.push({ 15 | "hash_type":{"plaintext": data[i]["hash_type"]}, 16 | "hash_value": {"plaintext": data[i]["hash_value"], "copyIcon": true}, 17 | "cracked": {"plaintext": data[i]["is_cracked"] ? "true": "false"}, 18 | "plaintext": {"plaintext": data[i]["plaintext_value"]}, 19 | "cracker": {"plaintext": data[i]["is_submitted_to_cracker"] ? "Submitted" : "Not Submitted"}, 20 | "actions": {"button": { 21 | "name": "Actions", 22 | "type": "menu", 23 | "value": [ 24 | { 25 | "name": "View All Data", 26 | "type": "dictionary", 27 | "value": data[i], 28 | "leftColumnTitle": "Key", 29 | "rightColumnTitle": "Value", 30 | "title": "Viewing Objective Data" 31 | }, 32 | ] 33 | }}, 34 | }); 35 | } 36 | return { 37 | "table": [ 38 | { 39 | "headers": [ 40 | {"plaintext": "hash_type", "type": "string", width: 100}, 41 | {"plaintext": "hash_value", "type": "string", fillWidth: true}, 42 | {"plaintext": "cracked", "type": "string", "width": 100}, 43 | {"plaintext": "plaintext", "type": "string", "fillWidth": true}, 44 | {"plaintext": "cracker", "type": "string", "width": 70}, 45 | {"plaintext": "actions", "type": "button", "width": 70}, 46 | ], 47 | "rows": output_table, 48 | "title": "Collected Hashes" 49 | } 50 | ] 51 | } 52 | }catch(error){ 53 | console.log(error); 54 | const combined = responses.reduce( (prev, cur) => { 55 | return prev + cur; 56 | }, ""); 57 | return {'plaintext': combined}; 58 | } 59 | }else{ 60 | return {"plaintext": "No output from command"}; 61 | } 62 | }else{ 63 | return {"plaintext": "No data to display..."}; 64 | } 65 | } -------------------------------------------------------------------------------- /Payload_Type/nemesis/nemesis/browser_scripts/triage.js: -------------------------------------------------------------------------------- 1 | function(task, responses){ 2 | if(task.status.includes("error")){ 3 | const combined = responses.reduce( (prev, cur) => { 4 | return prev + cur; 5 | }, ""); 6 | return {'plaintext': combined}; 7 | }else if(task.completed){ 8 | if(responses.length > 0){ 9 | try{ 10 | let data = JSON.parse(responses[0]); 11 | let output_table = []; 12 | for(let i = 0; i < data.length; i++){ 13 | output_table.push({ 14 | "value":{"plaintext": data[i]["value"], cellStyle: { 15 | backgroundColor: data[i]["value"] === "useful" ? "green" : data[i]["value"] === "notuseful" ? "red" : "orange", 16 | color: "white" 17 | }}, 18 | "operator": {"plaintext": data[i]["operator"], "copyIcon": true}, 19 | "name": {"plaintext": data[i]["file_data_enriched"]["name"]}, 20 | "magic_type": {"plaintext": data[i]["file_data_enriched"]["magic_type"]}, 21 | "nemesis type": {"plaintext": data[i]["file_data_enriched"]["nemesis_file_type"]}, 22 | "tags": {"plaintext": data[i]["file_data_enriched"]["tags"].join(", ")}, 23 | "actions": {"button": { 24 | "name": "Actions", 25 | "type": "menu", 26 | "value": [ 27 | { 28 | "name": "View All Data", 29 | "type": "dictionary", 30 | "value": data[i], 31 | "leftColumnTitle": "Key", 32 | "rightColumnTitle": "Value", 33 | "title": "Viewing Objective Data" 34 | }, 35 | ] 36 | }}, 37 | }); 38 | } 39 | return { 40 | "table": [ 41 | { 42 | "headers": [ 43 | {"plaintext": "value", "type": "string", width: 70}, 44 | {"plaintext": "operator", "type": "string", width: 100}, 45 | {"plaintext": "name", "type": "string", fillWidth: true}, 46 | {"plaintext": "magic_type", "type": "string", "fillWidth": true}, 47 | {"plaintext": "nemesis_type", "type": "string", "width": 70}, 48 | {"plaintext": "tags", "type": "string", "fillWidth": true}, 49 | {"plaintext": "actions", "type": "button", "width": 70}, 50 | ], 51 | "rows": output_table, 52 | "title": "Triaged Files" 53 | } 54 | ] 55 | } 56 | }catch(error){ 57 | console.log(error); 58 | const combined = responses.reduce( (prev, cur) => { 59 | return prev + cur; 60 | }, ""); 61 | return {'plaintext': combined}; 62 | } 63 | }else{ 64 | return {"plaintext": "No output from command"}; 65 | } 66 | }else{ 67 | return {"plaintext": "No data to display..."}; 68 | } 69 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nemesis 3 |

4 |
5 | 6 | # nemesis 7 | 8 | This is a Mythic agent for interacting with the 3rd party service, [Nemesis](https://github.com/SpecterOps/Nemesis). 9 | 10 | This doesn't build a payload, but instead generates a "callback" within Mythic that allows you to interact with Nemesis's API. 11 | 12 | Once you have Nemesis running, set your basic auth values as NEMESIS_USERNAME and NEMESIS_PASSWORD as user secrets in the Mythic UI (red key on your operator settings page). 13 | 14 | ## How to install an agent in this format within Mythic 15 | 16 | When it's time for you to test out your install or for another user to install your agent, it's pretty simple. Within Mythic you can run the `mythic-cli` binary to install this in one of three ways: 17 | 18 | * `sudo ./mythic-cli install github https://github.com/user/repo` to install the main branch 19 | * `sudo ./mythic-cli install github https://github.com/user/repo branchname` to install a specific branch of that repo 20 | * `sudo ./mythic-cli install folder /path/to/local/folder/cloned/from/github` to install from an already cloned down version of an agent repo 21 | 22 | Now, you might be wondering _when_ should you or a user do this to properly add your agent to their Mythic instance. There's no wrong answer here, just depends on your preference. The three options are: 23 | 24 | * Mythic is already up and going, then you can run the install script and just direct that agent's containers to start (i.e. `sudo ./mythic-cli start agentName` and if that agent has its own special C2 containers, you'll need to start them too via `sudo ./mythic-cli start c2profileName`). 25 | * Mythic is already up and going, but you want to minimize your steps, you can just install the agent and run `sudo ./mythic-cli start`. That script will first _stop_ all of your containers, then start everything back up again. This will also bring in the new agent you just installed. 26 | * Mythic isn't running, you can install the script and just run `sudo ./mythic-cli start`. 27 | 28 | ## Documentation 29 | 30 | View the rendered documentation by clicking on **Docs -> Agent Documentation** in the upper right-hand corner of the Mythic interface. -------------------------------------------------------------------------------- /agent_capabilities.json: -------------------------------------------------------------------------------- 1 | { 2 | "os": [ 3 | "Windows", 4 | "Linux", 5 | "macOS" 6 | ], 7 | "languages": [ 8 | "python" 9 | ], 10 | "features": { 11 | "mythic": [ 12 | "browser scripts", 13 | "docker" 14 | ], 15 | "custom": [] 16 | }, 17 | "payload_output": [], 18 | "architectures": [], 19 | "c2": [], 20 | "mythic_version": "3.3", 21 | "agent_version": "0.0.1", 22 | "supported_wrappers": [] 23 | } -------------------------------------------------------------------------------- /agent_icons/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MythicAgents/nemesis/242823dcb93473f8189ab4404618e13f3feaccc9/agent_icons/.keep -------------------------------------------------------------------------------- /agent_icons/nemesis.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude_payload_type": false, 3 | "exclude_c2_profiles": false, 4 | "exclude_documentation_payload": false, 5 | "exclude_documentation_c2": false, 6 | "exclude_agent_icons": false, 7 | "remote_images": { 8 | "nemesis": "ghcr.io/mythicagents/nemesis:v0.0.0.3" 9 | } 10 | } -------------------------------------------------------------------------------- /documentation-c2/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MythicAgents/nemesis/242823dcb93473f8189ab4404618e13f3feaccc9/documentation-c2/.keep -------------------------------------------------------------------------------- /documentation-payload/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MythicAgents/nemesis/242823dcb93473f8189ab4404618e13f3feaccc9/documentation-payload/.keep -------------------------------------------------------------------------------- /documentation-wrapper/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MythicAgents/nemesis/242823dcb93473f8189ab4404618e13f3feaccc9/documentation-wrapper/.keep --------------------------------------------------------------------------------