postgres version

This commit is contained in:
Your Name
2025-10-22 21:57:07 +03:00
commit 1aff196155
31 changed files with 10962 additions and 0 deletions

9
server/Dockerfile Normal file
View File

@@ -0,0 +1,9 @@
FROM python:3.12-alpine
COPY ./ /app/
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["./start_server.sh"]

14
server/__init__.py Normal file
View File

@@ -0,0 +1,14 @@
import logging
from flask import Flask
app = Flask(__name__)
app.logger.setLevel(logging.DEBUG)
for handler in app.logger.handlers:
handler.setLevel(logging.DEBUG)
import api
import views

34
server/api.py Normal file
View File

@@ -0,0 +1,34 @@
import time
from flask import request, jsonify
from __init__ import app
import auth, database, reloader
from models import FlagStatus
from spam import is_spam_flag
@app.route('/api/get_config')
@auth.api_auth_required
def get_config():
config = reloader.get_config()
return jsonify({key: value for key, value in config.items()
if 'PASSWORD' not in key and 'TOKEN' not in key})
@app.route('/api/post_flags', methods=['POST'])
@auth.api_auth_required
def post_flags():
flags = request.get_json()
flags = [item for item in flags if not is_spam_flag(item['flag'])]
cur_time = round(time.time())
rows = [(item['flag'], item['sploit'], item['team'], cur_time, FlagStatus.QUEUED.name)
for item in flags]
db = database.get()
cursor = db.cursor()
cursor.executemany("INSERT OR IGNORE INTO flags (flag, sploit, team, time, status) "
"VALUES (%s, %s, %s, %s, %s)", rows)
db.commit()
return ''

34
server/auth.py Normal file
View File

@@ -0,0 +1,34 @@
from functools import wraps
from flask import request, Response
import reloader
def authenticate():
return Response(
'Could not verify your access level for that URL. '
'You have to login with proper credentials', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'})
def auth_required(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.cookies.get('password')
config = reloader.get_config()
if auth is None or auth != config['SERVER_PASSWORD']:
return authenticate()
return f(*args, **kwargs)
return decorated
def api_auth_required(f):
@wraps(f)
def decorated(*args, **kwargs):
config = reloader.get_config()
if config['ENABLE_API_AUTH']:
if request.headers.get('X-Token', '') != config['API_TOKEN']:
return Response('Provided token is invalid.', 403)
return f(*args, **kwargs)
return decorated

45
server/config.py Normal file
View File

@@ -0,0 +1,45 @@
CONFIG = {
# Don't forget to remove the old database (flags.sqlite) before each competition.
'PORT': 8000,
# The clients will run sploits on TEAMS and
# fetch FLAG_FORMAT from sploits' stdout.
'TEAMS': {'Team #{}'.format(i): '10.0.0.{}'.format(i)
for i in range(1, 29 + 1)},
'FLAG_FORMAT': r'[A-Z0-9]{31}=',
# This configures how and where to submit flags.
# The protocol must be a module in protocols/ directory.
'SYSTEM_PROTOCOL': 'default',
'SYSTEM_HOST': '127.0.0.1',
'SYSTEM_PORT': 31337,
# 'SYSTEM_PROTOCOL': 'ructf_http',
# 'SYSTEM_URL': 'http://monitor.ructfe.org/flags',
# 'SYSTEM_TOKEN': 'your_secret_token',
# 'SYSTEM_PROTOCOL': 'volgactf',
# 'SYSTEM_HOST': '127.0.0.1',
# 'SYSTEM_PROTOCOL': 'forcad_tcp',
# 'SYSTEM_HOST': '127.0.0.1',
# 'SYSTEM_PORT': 31337,
# 'TEAM_TOKEN': 'your_secret_token',
# The server will submit not more than SUBMIT_FLAG_LIMIT flags
# every SUBMIT_PERIOD seconds. Flags received more than
# FLAG_LIFETIME seconds ago will be skipped.
'SUBMIT_FLAG_LIMIT': 50,
'SUBMIT_PERIOD': 5,
'FLAG_LIFETIME': 5 * 60,
# Password for the web interface. You can use it with any login.
# This value will be excluded from the config before sending it to farm clients.
'SERVER_PASSWORD': 'pepez_slit',
# Use authorization for API requests
'ENABLE_API_AUTH': False,
'API_TOKEN': '00000000000000000000'
}

201
server/database.py Normal file
View File

@@ -0,0 +1,201 @@
"""
Module with PostgreSQL helpers
"""
import os
import psycopg2
import threading
from psycopg2.extras import RealDictCursor
from flask import g
from __init__ import app
schema_path = os.path.join(os.path.dirname(__file__), 'schema.sql')
# Database configuration
db_host = os.getenv('DB_HOST', 'postgres')
db_port = os.getenv('DB_PORT', '5432')
db_name = os.getenv('DB_NAME', 'farmdb')
db_user = os.getenv('DB_USER', 'farm')
db_password = os.getenv('DB_PASSWORD', 'asdasdasd')
_init_started = False
_init_lock = threading.RLock()
def dict_factory(cursor, row):
"""
Convert database row to dictionary similar to sqlite3.Row
"""
return {col[0]: row[i] for i, col in enumerate(cursor.description)}
def get(context_bound=True):
"""
If there is no opened connection to the PostgreSQL database in the context
of the current request or if context_bound=False, get() opens a new
connection to the PostgreSQL database. Reopening the connection on each request
may have some overhead, but allows to avoid implementing a pool of
thread-local connections.
If the database schema needs initialization, get() creates and initializes it.
If get() is called from other threads at this time, they will wait
for the end of the initialization.
If context_bound=True, the connection will be closed after
request handling (when the context will be destroyed).
:returns: a connection to the initialized PostgreSQL database
"""
global _init_started
if context_bound and 'database' in g:
return g.database
# Connect to PostgreSQL database
database = psycopg2.connect(
host=db_host,
port=db_port,
dbname=db_name,
user=db_user,
password=db_password
)
# Check if initialization is needed
need_init = check_if_initialization_needed(database)
if need_init:
with _init_lock:
if not _init_started:
_init_started = True
_init(database)
if context_bound:
g.database = database
app.logger.info('DB connection established')
return database
def check_if_initialization_needed(conn):
"""
Check if database needs initialization by looking for specific tables.
Modify this based on your application's requirements.
"""
try:
cursor = conn.cursor()
# Check for existence of any table to see if DB is initialized
cursor.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
LIMIT 1
);
""")
tables_exist = cursor.fetchone()[0]
cursor.close()
return not tables_exist
except Exception as e:
app.logger.error(f"Error checking initialization status: {e}")
return True
def _init(conn):
"""
Initialize the database schema and any required data.
"""
try:
cursor = conn.cursor()
# Read and execute schema.sql if it exists
if os.path.exists(schema_path):
with open(schema_path, 'r') as f:
schema_sql = f.read()
cursor.execute(schema_sql)
app.logger.info("Executed schema.sql")
else:
# Fallback to basic tables if schema.sql doesn't exist
cursor.execute("""
CREATE TABLE IF NOT EXISTS flags (
id SERIAL PRIMARY KEY,
flag TEXT UNIQUE NOT NULL,
team INTEGER NOT NULL,
tick INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS submissions (
id SERIAL PRIMARY KEY,
flag TEXT NOT NULL,
team INTEGER NOT NULL,
tick INTEGER NOT NULL,
submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
""")
conn.commit()
cursor.close()
app.logger.info("Database initialized successfully")
except Exception as e:
conn.rollback()
app.logger.error(f"Database initialization failed: {e}")
raise
def query(sql, args=()):
"""
Execute a query and return results as dictionaries
"""
conn = get()
cursor = conn.cursor()
cursor.execute(sql, args)
if cursor.description: # If it's a SELECT query
results = [dict_factory(cursor, row) for row in cursor.fetchall()]
else: # For INSERT, UPDATE, DELETE
results = None
conn.commit()
cursor.close()
return results
def execute(sql, args=()):
"""
Execute a query that doesn't return results (INSERT, UPDATE, DELETE)
"""
conn = get()
cursor = conn.cursor()
cursor.execute(sql, args)
conn.commit()
cursor.close()
def fetch_one(sql, args=()):
"""
Execute a query and return first result as dictionary
"""
conn = get()
cursor = conn.cursor()
cursor.execute(sql, args)
if cursor.description: # If it's a SELECT query
row = cursor.fetchone()
result = dict_factory(cursor, row) if row else None
else:
result = None
conn.commit()
cursor.close()
return result
@app.teardown_appcontext
def close(_):
if 'database' in g:
g.database.close()

13
server/models.py Normal file
View File

@@ -0,0 +1,13 @@
from collections import namedtuple
from enum import Enum
class FlagStatus(Enum):
QUEUED = 0
SKIPPED = 1
ACCEPTED = 2
REJECTED = 3
Flag = namedtuple('Flag', ['flag', 'sploit', 'team', 'time', 'status', 'checksystem_response'])
SubmitResult = namedtuple('SubmitResult', ['flag', 'status', 'checksystem_response'])

View File

View File

@@ -0,0 +1,83 @@
import socket
from __init__ import app
from models import FlagStatus, SubmitResult
RESPONSES = {
FlagStatus.QUEUED: ['timeout', 'game not started', 'try again later', 'game over', 'is not up',
'no such flag'],
FlagStatus.ACCEPTED: ['accepted', 'congrat'],
FlagStatus.REJECTED: ['bad', 'wrong', 'expired', 'unknown', 'your own',
'too old', 'not in database', 'already submitted', 'invalid flag',
'self', 'invalid', 'already_submitted', 'team_not_found', 'too_old', 'stolen'],
}
READ_TIMEOUT = 5
APPEND_TIMEOUT = 0.05
BUFSIZE = 4096
def recvall(sock):
sock.settimeout(READ_TIMEOUT)
chunks = [sock.recv(BUFSIZE)]
sock.settimeout(APPEND_TIMEOUT)
while True:
try:
chunk = sock.recv(BUFSIZE)
if not chunk:
break
chunks.append(chunk)
except socket.timeout:
break
sock.settimeout(READ_TIMEOUT)
return b''.join(chunks)
def submit_flags(flags, config):
try:
sock = socket.create_connection((config['SYSTEM_HOST'], config['SYSTEM_PORT']),
READ_TIMEOUT)
except (socket.error, ConnectionRefusedError) as e:
app.logger.error(f"Failed to connect to checksystem: {e}")
# Return all flags as QUEUED since we couldn't submit them
for item in flags:
yield SubmitResult(item.flag, FlagStatus.QUEUED, "Connection failed")
return
try:
greeting = recvall(sock)
if b'Welcome' not in greeting:
raise Exception('Checksystem does not greet us: {}'.format(greeting))
sock.sendall(config['TEAM_TOKEN'].encode() + b'\n')
invite = recvall(sock)
if b'enter your flags' not in invite:
raise Exception('Team token seems to be invalid: {}'.format(invite))
unknown_responses = set()
for item in flags:
sock.sendall(item.flag.encode() + b'\n')
response = recvall(sock).decode().strip()
if response:
response = response.splitlines()[0]
response = response.replace('[{}] '.format(item.flag), '')
response_lower = response.lower()
for status, substrings in RESPONSES.items():
if any(s in response_lower for s in substrings):
found_status = status
break
else:
found_status = FlagStatus.QUEUED
if response not in unknown_responses:
unknown_responses.add(response)
app.logger.warning('Unknown checksystem response (flag will be resent): %s', response)
yield SubmitResult(item.flag, found_status, response)
finally:
sock.close()

View File

@@ -0,0 +1,76 @@
# Based on https://gist.github.com/xmikasax/90a0ce5736a4274e46b9958f836951e7
import socket
from __init__ import app
from models import FlagStatus, SubmitResult
RESPONSES = {
FlagStatus.QUEUED: ['timeout', 'game not started', 'try again later', 'game over', 'is not up',
'no such flag'],
FlagStatus.ACCEPTED: ['accepted', 'congrat'],
FlagStatus.REJECTED: ['bad', 'wrong', 'expired', 'unknown', 'your own',
'too old', 'not in database', 'already submitted', 'invalid flag',
'self', 'invalid', 'already_submitted', 'team_not_found', 'too_old', 'stolen'],
}
READ_TIMEOUT = 5
APPEND_TIMEOUT = 0.05
BUFSIZE = 4096
def recvall(sock):
sock.settimeout(READ_TIMEOUT)
chunks = [sock.recv(BUFSIZE)]
sock.settimeout(APPEND_TIMEOUT)
while True:
try:
chunk = sock.recv(BUFSIZE)
if not chunk:
break
chunks.append(chunk)
except socket.timeout:
break
sock.settimeout(READ_TIMEOUT)
return b''.join(chunks)
def submit_flags(flags, config):
sock = socket.create_connection((config['SYSTEM_HOST'], config['SYSTEM_PORT']),
READ_TIMEOUT)
greeting = recvall(sock)
if b'Welcome' not in greeting:
raise Exception('Checksystem does not greet us: {}'.format(greeting))
sock.sendall(config['TEAM_TOKEN'].encode() + b'\n')
invite = recvall(sock)
if b'enter your flags' not in invite:
raise Exception('Team token seems to be invalid: {}'.format(invite))
unknown_responses = set()
for item in flags:
sock.sendall(item.flag.encode() + b'\n')
response = recvall(sock).decode().strip()
if response:
response = response.splitlines()[0]
response = response.replace('[{}] '.format(item.flag), '')
response_lower = response.lower()
for status, substrings in RESPONSES.items():
if any(s in response_lower for s in substrings):
found_status = status
break
else:
found_status = FlagStatus.QUEUED
if response not in unknown_responses:
unknown_responses.add(response)
app.logger.warning('Unknown checksystem response (flag will be resent): %s', response)
yield SubmitResult(item.flag, found_status, response)
sock.close()

View File

@@ -0,0 +1,46 @@
import requests
from __init__ import app
from models import FlagStatus, SubmitResult
RESPONSES = {
FlagStatus.QUEUED: ['timeout', 'game not started', 'try again later', 'game over', 'is not up',
'no such flag'],
FlagStatus.ACCEPTED: ['accepted', 'congrat'],
FlagStatus.REJECTED: ['bad', 'wrong', 'expired', 'unknown', 'your own',
'too old', 'not in database', 'already submitted', 'invalid flag'],
}
# The RuCTF checksystem adds a signature to all correct flags. It returns
# "invalid flag" verdict if the signature is invalid and "no such flag" verdict if
# the signature is correct but the flag was not found in the checksystem database.
#
# The latter situation happens if a checker puts the flag to the service before putting it
# to the checksystem database. We should resent the flag later in this case.
TIMEOUT = 5
def submit_flags(flags, config):
r = requests.put(config['SYSTEM_URL'],
headers={'X-Team-Token': config['SYSTEM_TOKEN']},
json=[item.flag for item in flags], timeout=TIMEOUT)
unknown_responses = set()
for item in r.json():
response = item['msg'].strip()
response = response.replace('[{}] '.format(item['flag']), '')
response_lower = response.lower()
for status, substrings in RESPONSES.items():
if any(s in response_lower for s in substrings):
found_status = status
break
else:
found_status = FlagStatus.QUEUED
if response not in unknown_responses:
unknown_responses.add(response)
app.logger.warning('Unknown checksystem response (flag will be resent): %s', response)
yield SubmitResult(item['flag'], found_status, response)

View File

@@ -0,0 +1,74 @@
import socket
from __init__ import app
from models import FlagStatus, SubmitResult
RESPONSES = {
FlagStatus.QUEUED: ['timeout', 'game not started', 'try again later', 'game over', 'is not up',
'no such flag'],
FlagStatus.ACCEPTED: ['accepted', 'congrat'],
FlagStatus.REJECTED: ['bad', 'wrong', 'expired', 'unknown', 'your own',
'too old', 'not in database', 'already submitted', 'invalid flag'],
}
# The RuCTF checksystem adds a signature to all correct flags. It returns
# "invalid flag" verdict if the signature is invalid and "no such flag" verdict if
# the signature is correct but the flag was not found in the checksystem database.
#
# The latter situation happens if a checker puts the flag to the service before putting it
# to the checksystem database. We should resent the flag later in this case.
READ_TIMEOUT = 5
APPEND_TIMEOUT = 0.05
BUFSIZE = 4096
def recvall(sock):
sock.settimeout(READ_TIMEOUT)
chunks = [sock.recv(BUFSIZE)]
sock.settimeout(APPEND_TIMEOUT)
while True:
try:
chunk = sock.recv(BUFSIZE)
if not chunk:
break
chunks.append(chunk)
except socket.timeout:
break
sock.settimeout(READ_TIMEOUT)
return b''.join(chunks)
def submit_flags(flags, config):
sock = socket.create_connection((config['SYSTEM_HOST'], config['SYSTEM_PORT']),
READ_TIMEOUT)
greeting = recvall(sock)
if b'Enter your flags' not in greeting:
raise Exception('Checksystem does not greet us: {}'.format(greeting))
unknown_responses = set()
for item in flags:
sock.sendall(item.flag.encode() + b'\n')
response = recvall(sock).decode().strip()
if response:
response = response.splitlines()[0]
response = response.replace('[{}] '.format(item.flag), '')
response_lower = response.lower()
for status, substrings in RESPONSES.items():
if any(s in response_lower for s in substrings):
found_status = status
break
else:
found_status = FlagStatus.QUEUED
if response not in unknown_responses:
unknown_responses.add(response)
app.logger.warning('Unknown checksystem response (flag will be resent): %s', response)
yield SubmitResult(item.flag, found_status, response)
sock.close()

View File

@@ -0,0 +1,26 @@
from themis.finals.attack.helper import Helper
from themis.finals.attack.result import Result
from models import FlagStatus, SubmitResult
RESPONSES = {
FlagStatus.ACCEPTED: [Result.SUCCESS_FLAG_ACCEPTED],
FlagStatus.REJECTED: [Result.ERROR_FLAG_EXPIRED, Result.ERROR_FLAG_YOURS,
Result.ERROR_FLAG_SUBMITTED, Result.ERROR_FLAG_NOT_FOUND],
}
def submit_flags(flags, config):
h = Helper(config['SYSTEM_HOST'])
codes = h.attack(*[item.flag for item in flags])
for item, code in zip(flags, codes):
for status, possible_codes in RESPONSES.items():
if code in possible_codes:
found_status = status
break
else:
found_status = FlagStatus.QUEUED
yield SubmitResult(item.flag, found_status, code.name)

49
server/reloader.py Normal file
View File

@@ -0,0 +1,49 @@
import importlib
import os
import threading
import importlib.util
from __init__ import app
_config_mtime = None
_reload_lock = threading.RLock()
if "CONFIG" in os.environ:
config_path = os.environ["CONFIG"]
else:
config_path = os.path.join(app.root_path, 'config.py')
config_spec = importlib.util.spec_from_file_location("server.config", config_path)
config_module = importlib.util.module_from_spec(config_spec)
config_spec.loader.exec_module(config_module)
_cur_config = config_module.CONFIG
def get_config():
"""
Returns CONFIG dictionary from config.py module.
If config.py file was updated since the last call, get_config() reloads
the dictionary. If an error happens during reloading, get_config() returns
the old dictionary.
:returns: the newest valid version of the CONFIG dictionary
"""
global _config_mtime, _cur_config
cur_mtime = os.stat(config_path).st_mtime_ns
if cur_mtime != _config_mtime:
with _reload_lock:
if cur_mtime != _config_mtime:
try:
config_spec.loader.exec_module(config_module)
_cur_config = config_module.CONFIG
app.logger.info('New config loaded')
except Exception as e:
app.logger.error('Failed to reload config: %s', e)
_config_mtime = cur_mtime
return _cur_config

14
server/requirements.txt Normal file
View File

@@ -0,0 +1,14 @@
certifi>=2018.1.18
chardet>=3.0.4
click>=6.7
Flask>=1.1.1
idna>=2.6
itsdangerous>=0.24
Jinja2>=2.11.3
MarkupSafe>=1.1.1
requests>=2.22.0
themis.finals.attack.helper>=1.1.0
themis.finals.attack.result>=1.3.0
urllib3>=1.26.5
Werkzeug>=0.16.0
psycopg2-binary

13
server/schema.sql Normal file
View File

@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS flags (
flag TEXT PRIMARY KEY,
sploit TEXT,
team TEXT,
time INTEGER,
status TEXT,
checksystem_response TEXT
);
CREATE INDEX IF NOT EXISTS flags_sploit ON flags(sploit);
CREATE INDEX IF NOT EXISTS flags_team ON flags(team);
CREATE INDEX IF NOT EXISTS flags_status_time ON flags(status, time);
CREATE INDEX IF NOT EXISTS flags_time ON flags(time);

37
server/spam.py Normal file
View File

@@ -0,0 +1,37 @@
def generate_spam_flag():
import base64, hashlib, os, re
encode = lambda s: re.sub(r'[a-z/+=\n]', r'', base64.encodebytes(s).decode()).upper()
secret = '1234'
prefix = encode(os.urandom(64))[:16]
suffix = encode(hashlib.sha256((prefix + secret).encode()).digest())[:15]
return prefix + suffix + '='
def is_spam_flag(flag):
import base64, hashlib, re
encode = lambda s: re.sub(r'[a-z/+=\n]', r'', base64.encodebytes(s).decode()).upper()
secret = '1234'
prefix = flag[:16].upper()
suffix = encode(hashlib.sha256((prefix + secret).encode()).digest())[:15]
return flag[16:].upper() == suffix + '='
def test():
import base64, hashlib, os, re
encode = lambda s: re.sub(r'[a-z/+=\n]', r'', base64.encodebytes(s).decode()).upper()
for i in range(10**4):
flag = encode(os.urandom(128))[:31] + '='
if i < 30:
print(flag)
assert not is_spam_flag(flag)
for i in range(10**3):
assert is_spam_flag(generate_spam_flag())
print('Ok')
if __name__ == '__main__':
test()

12
server/standalone.py Normal file
View File

@@ -0,0 +1,12 @@
import threading
import werkzeug.serving
from __init__ import app
import submit_loop
if not werkzeug.serving.is_running_from_reloader():
submit_thread = threading.Thread(target=submit_loop.run_loop)
submit_thread.start()

12
server/start_docker.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# Get port from config.py
PORT=$(python3 -c "from config import CONFIG; print(CONFIG.get('SYSTEM_PORT'))")
WEB_PORT=$(python3 -c "from config import CONFIG; print(CONFIG.get('PORT'))")
# Build and run docker with port forwarding
if [ "$1" = "--build" ]; then
docker build -t pb_farm .
fi
docker run --rm -p $PORT:$PORT pb_farm

6
server/start_server.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
# Use FLASK_DEBUG=True if needed
PORT=$(python3 -c "from config import CONFIG; print(CONFIG.get('PORT'))")
FLASK_APP=$(dirname $(readlink -f $0))/standalone.py python3 -m flask run --host 0.0.0.0 --with-threads --port $PORT

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
@import url("https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,700italic,400,300,700");
.navbar-brand {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 19px;
font-weight: 300;
padding: 15px 15px;
color: #e52250;
}
.logo {
height: 30px;
}
.table td {
font-size: 14px;
padding: 0.65rem 0.75rem;
}
textarea {
resize: none;
}
#text-with-flags {
height: 80px;
}

BIN
server/static/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

5
server/static/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

144
server/static/js/ui.js Normal file
View File

@@ -0,0 +1,144 @@
function padLeft(s, length) {
s = s.toString();
while (s.length < length)
s = '0' + s;
return s;
}
function dateToString(date) {
return padLeft(date.getFullYear(), 4) + '-' + padLeft(date.getMonth() + 1, 2) + '-' +
padLeft(date.getDate(), 2) + ' ' +
padLeft(date.getHours(), 2) + ':' + padLeft(date.getMinutes(), 2) + ':' +
padLeft(date.getSeconds(), 2);
}
function escapeHtml(text) {
return $('<div>').text(text).html();
}
function generateFlagTableRows(rows) {
var html = '';
rows.forEach(function (item) {
var cells = [
item.sploit,
item.team !== null ? item.team : '',
item.flag,
dateToString(new Date(item.time * 1000)),
item.status,
item.checksystem_response !== null ? item.checksystem_response : ''
];
html += '<tr>';
cells.forEach(function (text) {
html += '<td>' + escapeHtml(text) + '</td>';
});
html += '</tr>';
});
return html;
}
function generatePaginator(totalCount, rowsPerPage, pageNumber) {
var totalPages = Math.ceil(totalCount / rowsPerPage);
var firstShown = Math.max(1, pageNumber - 3);
var lastShown = Math.min(totalPages, pageNumber + 3);
var html = '';
if (firstShown > 1)
html += '<li class="page-item"><a class="page-link" href="#" data-content="1">«</a></li>';
for (var i = firstShown; i <= lastShown; i++) {
var extraClasses = (i === pageNumber ? "active" : "");
html += '<li class="page-item ' + extraClasses + '">' +
'<a class="page-link" href="#" data-content="' + i + '">' + i + '</a>' +
'</li>';
}
if (lastShown < totalPages)
html += '<li class="page-item">' +
'<a class="page-link" href="#" data-content="' + totalPages + '">»</a>' +
'</li>';
return html;
}
function getPageNumber() {
return parseInt($('#page-number').val());
}
function setPageNumber(number) {
$('#page-number').val(number);
}
var queryInProgress = false;
function showFlags() {
if (queryInProgress)
return;
queryInProgress = true;
$('.search-results').hide();
$('.query-status').html('Loading...').show();
$.post('/ui/show_flags', $('#show-flags-form').serialize())
.done(function (response) {
$('.search-results tbody').html(generateFlagTableRows(response.rows));
$('.search-results .total-count').text(response.total_count);
$('.search-results .pagination').html(generatePaginator(
response.total_count, response.rows_per_page, getPageNumber()));
$('.search-results .page-link').click(function (event) {
event.preventDefault();
setPageNumber($(this).data("content"));
showFlags();
});
$('.query-status').hide();
$('.search-results').show();
})
.fail(function () {
$('.query-status').html("Failed to load flags from the farm server");
})
.always(function () {
queryInProgress = false;
});
}
function postFlagsManual() {
if (queryInProgress)
return;
queryInProgress = true;
$.post('/ui/post_flags_manual', $('#post-flags-manual-form').serialize())
.done(function () {
var sploitSelect = $('#sploit-select');
if ($('#sploit-manual-option').empty())
sploitSelect.append($('<option id="sploit-manual-option">Manual</option>'));
sploitSelect.val('Manual');
$('#team-select, #flag-input, #time-since-input, #time-until-input, ' +
'#status-select, #checksystem-response-input').val('');
queryInProgress = false;
showFlags();
})
.fail(function () {
$('.query-status').html("Failed to post flags to the farm server");
queryInProgress = false;
});
}
$(function () {
showFlags();
$('#show-flags-form').submit(function (event) {
event.preventDefault();
setPageNumber(1);
showFlags();
});
$('#post-flags-manual-form').submit(function (event) {
event.preventDefault();
postFlagsManual();
});
});

BIN
server/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

95
server/submit_loop.py Executable file
View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3
import importlib
import random
import time
from collections import defaultdict
from __init__ import app
import database
import reloader
from models import Flag, FlagStatus, SubmitResult
def get_fair_share(groups, limit):
if not groups:
return []
groups = sorted(groups, key=len)
places_left = limit
group_count = len(groups)
fair_share = places_left // group_count
result = []
residuals = []
for group in groups:
if len(group) <= fair_share:
result += group
places_left -= len(group)
group_count -= 1
if group_count > 0:
fair_share = places_left // group_count
# The fair share could have increased because the processed group
# had a few elements. Sorting order guarantees that the smaller
# groups will be processed first.
else:
selected = random.sample(group, fair_share + 1)
result += selected[:-1]
residuals.append(selected[-1])
result += random.sample(residuals, min(limit - len(result), len(residuals)))
random.shuffle(result)
return result
def submit_flags(flags, config):
module = importlib.import_module('protocols.' + config['SYSTEM_PROTOCOL'])
try:
return list(module.submit_flags(flags, config))
except Exception as e:
message = '{}: {}'.format(type(e).__name__, str(e))
app.logger.exception('Exception on submitting flags')
return [SubmitResult(item.flag, FlagStatus.QUEUED, message) for item in flags]
def run_loop():
app.logger.info('Starting submit loop')
with app.app_context():
db = database.get(context_bound=False)
cursor = db.cursor()
while True:
submit_start_time = time.time()
config = reloader.get_config()
skip_time = round(submit_start_time - config['FLAG_LIFETIME'])
cursor.execute("UPDATE flags SET status = %s WHERE status = %s AND time < %s",
(FlagStatus.SKIPPED.name, FlagStatus.QUEUED.name, skip_time))
db.commit()
cursor.execute("SELECT * FROM flags WHERE status = %s", (FlagStatus.QUEUED.name,))
queued_flags = [Flag(**item) for item in cursor.fetchall()]
if queued_flags:
grouped_flags = defaultdict(list)
for item in queued_flags:
grouped_flags[item.sploit, item.team].append(item)
flags = get_fair_share(grouped_flags.values(), config['SUBMIT_FLAG_LIMIT'])
app.logger.debug('Submitting %s flags (out of %s in queue)', len(flags), len(queued_flags))
results = submit_flags(flags, config)
rows = [(item.status.name, item.checksystem_response, item.flag) for item in results]
cursor.executemany("UPDATE flags SET status = %s, checksystem_response = %s"
"WHERE flag = %s", rows)
db.commit()
submit_spent = time.time() - submit_start_time
if config['SUBMIT_PERIOD'] > submit_spent:
time.sleep(config['SUBMIT_PERIOD'] - submit_spent)
if __name__ == "__main__":
run_loop()

View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<title>Login Required</title>
<style>
body {
font-family: sans-serif;
max-width: 500px;
margin: 40px auto;
padding: 20px;
}
.login-form {
border: 1px solid #ccc;
padding: 20px;
border-radius: 4px;
}
input[type="password"] {
width: 100%;
padding: 8px;
margin: 10px 0;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
input[type="submit"] {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div class="login-form">
<h2>Добро пожаловать!</h2>
<form method="POST">
<label for="password">Пароль:</label>
<input type="password" id="password" name="password" required>
<input type="submit" value="Войти">
</form>
</div>
</body>
</html>

150
server/templates/index.html Normal file
View File

@@ -0,0 +1,150 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="/static/logo.png">
<title>Poison Berries Farm - Server</title>
<link href="/static/css/bootswatch-flatly.css" rel="stylesheet">
<link href="/static/css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark py-0">
<img class="logo" src="/static/img/logo.png">
<div class="navbar-brand">Poison Berries Farm</div>
</nav>
<div class="container mt-4">
<div class="row mb-4">
<div class="col-lg-8">
<div class="card border-light">
<div class="card-body">
<h4 class="card-title">Show Flags</h4>
<form id="show-flags-form">
<div class="row mb-2">
<div class="col-md-4">
<label for="sploit-select">Sploit</label>
<select class="form-control form-control-sm" id="sploit-select" name="sploit">
<option value="">All</option>
{% for item in distinct_values['sploit'] %}
<option>{{ item }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label for="team-select">Team</label>
<select class="form-control form-control-sm" id="team-select" name="team">
<option value="">All</option>
{% for item in distinct_values['team'] %}
<option>{{ item }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label for="flag-input">
Flag
<small class="text-muted ml-2">substring, case-insensitive</small>
</label>
<input type="text" class="form-control form-control-sm" id="flag-input" name="flag">
</div>
</div>
<div class="row mb-3">
<div class="col-md-3">
<label for="time-since-input">
Since
<small class="text-muted ml-2">{{ server_tz_name }}</small>
</label>
<input type="text" class="form-control form-control-sm" id="time-since-input"
name="time-since"
placeholder="yyyy-mm-dd hh:mm">
</div>
<div class="col-md-3">
<label for="time-until-input">
Until
<small class="text-muted ml-2">{{ server_tz_name }}</small>
</label>
<input type="text" class="form-control form-control-sm" id="time-until-input"
name="time-until"
placeholder="yyyy-mm-dd hh:mm">
</div>
<div class="col-md-2">
<label for="status-select">Status</label>
<select class="form-control form-control-sm" id="status-select" name="status">
<option value="">All</option>
{% for item in distinct_values['status'] %}
<option>{{ item }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label for="checksystem-response-input">
Checksystem response
</label>
<input type="text" class="form-control form-control-sm" id="checksystem-response-input"
name="checksystem_response">
</div>
</div>
<div class="row">
<div class="col-12">
<button type="submit" class="btn btn-primary btn-sm submit-btn">Show</button>
</div>
</div>
<input type="hidden" value="1" id="page-number" name="page-number">
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-light">
<div class="card-body">
<h4 class="card-title">Add Flags Manually</h4>
<form id="post-flags-manual-form">
<label for="text-with-flags">
Text with flags
<small class="text-muted ml-2">flag format: {{ flag_format }}</small>
</label>
<textarea class="form-control form-control-sm mb-3" id="text-with-flags" name="text"></textarea>
<button type="submit" class="btn btn-primary btn-sm">Send</button>
</form>
</div>
</div>
</div>
</div>
<div class="mb-3 text-center query-status"></div>
<div class="search-results" style="display: none;">
<p>Found <span class="total-count"></span> flags</p>
<ul class="pagination pagination-sm"></ul>
<table class="table table-hover">
<thead>
<tr class="table-secondary">
<th scope="col">Sploit</th>
<th scope="col">Team</th>
<th scope="col">Flag</th>
<th scope="col">Time</th>
<th scope="col">Status</th>
<th scope="col">Checksystem Response</th>
</tr>
</thead>
<tbody></tbody>
</table>
<ul class="pagination pagination-sm"></ul>
</div>
</div>
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/ui.js"></script>
</body>
</html>

119
server/views.py Normal file
View File

@@ -0,0 +1,119 @@
import re
import time
from datetime import datetime
from flask import jsonify, render_template, request, redirect
from __init__ import app
import auth
import database
import reloader
from models import FlagStatus
@app.template_filter('timestamp_to_datetime')
def timestamp_to_datetime(s):
return datetime.fromtimestamp(s)
@app.route('/', methods=['GET', 'POST'])
def index_redirect():
if request.method == 'POST':
response = redirect('/')
response.set_cookie('password', request.form['password'])
return response
config = reloader.get_config()
if request.cookies.get('password') == config['SERVER_PASSWORD']:
return redirect('/farm')
return render_template('hello.html')
@app.route('/farm')
@auth.auth_required
def index():
distinct_values = {}
for column in ['sploit', 'status', 'team']:
rows = database.query('SELECT DISTINCT {} FROM flags ORDER BY {}'.format(column, column))
distinct_values[column] = [item[column] for item in rows] # Access by key, not index
config = reloader.get_config()
server_tz_name = time.strftime('%Z')
if server_tz_name.startswith('+'):
server_tz_name = 'UTC' + server_tz_name
return render_template('index.html',
flag_format=config['FLAG_FORMAT'],
distinct_values=distinct_values,
server_tz_name=server_tz_name)
FORM_DATETIME_FORMAT = '%Y-%m-%d %H:%M'
FLAGS_PER_PAGE = 30
@app.route('/ui/show_flags', methods=['POST'])
@auth.auth_required
def show_flags():
conditions = []
for column in ['sploit', 'status', 'team']:
value = request.form[column]
if value:
conditions.append(('{} = %s'.format(column), value))
for column in ['flag', 'checksystem_response']:
value = request.form[column]
if value:
conditions.append(('LOWER({}) LIKE %s'.format(column), '%' + value.lower() + '%')) # Changed INSTR to LIKE
for param in ['time-since', 'time-until']:
value = request.form[param].strip()
if value:
timestamp = round(datetime.strptime(value, FORM_DATETIME_FORMAT).timestamp())
sign = '>=' if param == 'time-since' else '<='
conditions.append(('time {} %s'.format(sign), timestamp))
page_number = int(request.form['page-number'])
if page_number < 1:
raise ValueError('Invalid page-number')
if conditions:
chunks, values = list(zip(*conditions))
conditions_sql = 'WHERE ' + ' AND '.join(chunks)
conditions_args = list(values)
else:
conditions_sql = ''
conditions_args = []
sql = 'SELECT * FROM flags ' + conditions_sql + ' ORDER BY time DESC LIMIT %s OFFSET %s'
args = conditions_args + [FLAGS_PER_PAGE, FLAGS_PER_PAGE * (page_number - 1)]
flags = database.query(sql, args)
sql = 'SELECT COUNT(*) as count FROM flags ' + conditions_sql # Added alias
args = conditions_args
total_count_result = database.query(sql, args)
total_count = total_count_result[0]['count'] if total_count_result else 0 # Access by key
return jsonify({
'rows': [dict(item) for item in flags],
'rows_per_page': FLAGS_PER_PAGE,
'total_count': total_count,
})
@app.route('/ui/post_flags_manual', methods=['POST'])
@auth.auth_required
def post_flags_manual():
config = reloader.get_config()
flags = re.findall(config['FLAG_FORMAT'], request.form['text'])
cur_time = round(time.time())
rows = [(item, 'Manual', '*', cur_time, FlagStatus.QUEUED.name)
for item in flags]
app.logger.info('Inserting flags: %s', rows)
# Use the execute function instead of direct db access
for row in rows:
database.execute(
"INSERT INTO flags (flag, sploit, team, time, status) VALUES (%s, %s, %s, %s, %s) ON CONFLICT (flag) DO NOTHING",
row
)
return ''
@app.route('/robots.txt', methods=['GET'])
def robots_txt():
return 'User-agent: *\nDisallow: /'