Initial commit

This commit is contained in:
connorgadbois 2026-03-27 12:27:18 -05:00
commit 64c58bde76
9 changed files with 228 additions and 0 deletions

26
README.md Normal file
View File

@ -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
```

13
ttf-server/config.yml Normal file
View File

@ -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

View File

@ -0,0 +1,2 @@
psycopg2-binary
peewee

View File

@ -0,0 +1,4 @@
import ttf
server = ttf.TTFServer(ttf.config.config['ttf_host'], ttf.config.config['ttf_port'])
server.start()

View File

@ -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

28
ttf-server/ttf/config.py Normal file
View File

@ -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'),
}

View File

@ -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])

17
ttf-server/ttf/logging.py Normal file
View File

@ -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)

79
ttf-server/ttf/server.py Normal file
View File

@ -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}')