From 94f046ab22e0c5a74778f84b54898703ba6770af Mon Sep 17 00:00:00 2001 From: sourque Date: Fri, 3 Jul 2020 03:28:06 -0700 Subject: [PATCH] Implement source IP tracking and automatic CSS team creation --- README.md | 14 +++++++-- engine/db.py | 49 +++++++++++++++++++++++--------- engine/engine.py | 5 +++- engine/server.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 123 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index dded40d..e8e7eda 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Minos Scoring Engine -This is a scoring engine meant to imitate the functionality of UTSA's CIAS CyberPatriot Scoring Engine with an emphasis on simplicity. It acts as an uptime scorer (ex. your service has been up 50% of the time, and is down right now). It is based on DSU's DefSec Club [Scoring Engine](https://github.com/DSUDefSec/ScoringEngine). Named after the Greek myth of King Minos, judge of the dead. +Minos acts as a service uptime scorer (ex. your service has been up 50% of the time, and is down right now). It is based on DSU's DefSec Club [Scoring Engine](https://github.com/DSUDefSec/ScoringEngine). ## Installation @@ -215,7 +215,7 @@ password = "HackersArentReal" teams = [ "TEAM-12398fhn", "TEAM-qwertyui" ] -# If specified, replace the above team IDs +# If specified, replace the below team IDs # with the below aliases (one to one) # Note: aliases without team IDs above # will cause an error. A greater number @@ -225,6 +225,14 @@ teams = [ "TEAM-12398fhn", team_aliases = [ "team1", "team2" ] +# If specified, the below emails will be +# included in the CSV report, matching the index +# of the team ids and aliases. This can be any +# identifier that you don't want to be in the unique +# id nor the scoreboard +team_emails = [ "team1email@example.org", + "team2email@example.org" ] + # If specified, only allow score updates # for the following image names images = [ "supercoolimage1", @@ -276,7 +284,7 @@ points = 500 **Service won't restart** ![no_restart](setup/imgs/broken_restart.png) -- On some operating systems (notably DigitalOcean boxes), the init.d script fails to manage the service correctly. +- In some operating environments (notably DigitalOcean boxes), the init.d script fails to manage the service correctly. - You can manually kill the python and uwsgi processes. - `sudo pkill -9 python3; pkill -p uwsgi` diff --git a/engine/db.py b/engine/db.py index 343c0ec..3817311 100644 --- a/engine/db.py +++ b/engine/db.py @@ -39,8 +39,8 @@ def get_uid(username): def engine_status(): config = read_running_config() - if config and config["settings"]["running"] == 1: - return True + if "settings" in config and "running" in config["settings"] and config["settings"]["running"] == 1: + return True else: return False @@ -165,7 +165,7 @@ def insert_totals_score(team, type, points, check_round): # CSS FUNCTIONS # ################# -def get_css_csv(remote): +def get_css_csv(remote, ips): http_csv = str.encode("") teams = get_css_teams(remote) images = get_css_images(remote) @@ -176,7 +176,11 @@ def get_css_csv(remote): for image in images: try: image_sum = execute("SELECT `points` FROM `css_results` WHERE team=? AND image=? ORDER BY time DESC", (team, image), one=True)[0] - http_csv += str.encode(find_alias(index, remote) + "," + team + "," + image + "," + str(image_sum) + "," + get_css_play_time(team, image=image) + "," + get_css_elapsed_time(team) + "\n") + if team in ips: + ip = ips[team] + else: + ip = "N/A" + http_csv += str.encode(find_email(index, remote) + "," + find_alias(index, remote) + "," + team + "," + image + "," + str(image_sum) + "," + ip + "," + get_css_play_time(team, image=image) + "," + get_css_elapsed_time(team) + "\n") except: # Ignoring images that a team hasn't started yet pass @@ -229,6 +233,8 @@ def get_css_scores(remote): team_scores.reverse() return(team_scores) + +# TODO optimize this def get_css_score(team, remote): image_data = {} @@ -296,7 +302,6 @@ def get_css_score(team, remote): image[4] = "|-|".join(vuln_info[2:]) except Exception as e: print("[ERROR] Error decoding hex vulns from databases! (" + str(e) + ")") - return(labels, image_data, scores) def get_css_elapsed_time(team): @@ -335,11 +340,23 @@ def insert_css_score(team, image, points, vulns): execute("INSERT INTO `css_results` ('team', 'image', 'points', 'vulns') VALUES (?, ?, ?, ?)", (team, image, points, vulns)) def find_alias(team_index, remote): - aliases = remote["team_aliases"] + try: + aliases = remote["team_aliases"] + except: + aliases = [] if not team_index > len(aliases) - 1: return aliases[team_index] return "N/A" +def find_email(team_index, remote): + try: + emails = remote["team_emails"] + except: + emails = [] + if not team_index > len(emails) - 1: + return emails[team_index] + return "N/A" + def apply_aliases(team_scores, remote): for score_index, team in enumerate(team_scores): for index, team_id in enumerate(remote["teams"]): @@ -376,6 +393,12 @@ def validate_alphanum(string): return True return False +def validate_email(string): + # Courtesy of regular-expressions.info + if re.compile("(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)").match(string): + return True + return False + def copy_config(): print("[INFO] Copying config into running-config...") write_running_config(read_config()) @@ -388,22 +411,22 @@ def read_config(): def read_running_config(): try: with open(path + 'engine/running-config.cfg', 'r') as f: - config = toml.load(f) - if "settings" not in config: - config["settings"] = {} + config_tmp = toml.load(f) + if "settings" not in config_tmp: + config_tmp["settings"] = {} except OSError: print("[WARN] File running-config.cfg not found.") - config = None - return config + config_tmp = None + return config_tmp def get_css_colors(): try: return read_config()["remote"]["colors"] except: pass -def write_running_config(config): +def write_running_config(config_tmp): with open(path + 'engine/running-config.cfg', 'w') as f: - toml.dump(config, f) + toml.dump(config_tmp, f) def stop_engine(): config = read_running_config() diff --git a/engine/engine.py b/engine/engine.py index d490dd6..ca4f997 100644 --- a/engine/engine.py +++ b/engine/engine.py @@ -194,7 +194,10 @@ def start(): em = EngineModel() while True: em.load() - running = em.settings['running'] + if "running" in em.settings: + running = em.settings['running'] + else: + running = 1 if "interval" in em.settings: interval = em.settings['interval'] else: diff --git a/engine/server.py b/engine/server.py index 8b6f8c7..4c423df 100644 --- a/engine/server.py +++ b/engine/server.py @@ -15,16 +15,21 @@ wm = WebModel() em = engine.EngineModel() app = Flask(__name__) -app.secret_key = 'this is a secret!! lol' +# Should ideally store key in config file-- but this works pretty well +app.secret_key = os.urandom(24).hex() login_manager = LoginManager() login_manager.init_app(app) +# CSS hardcoded values +permitted_new_css_ip = "10.10.0.2" # Put your whitelisted IP here + # Caching and refreshing... refresh_threshold = timedelta(seconds=15) scoreboard_time_refresh = datetime.now() - refresh_threshold team_time_refresh = datetime.now() - refresh_threshold team_scores = None # For scoreboard team_data = {} # For details +ips = {} # Stalking incoming IPs for CSS leaderboard :eyes: @login_manager.user_loader def load_user(uid): @@ -292,12 +297,77 @@ def css_update(): if success: vulns = db.printAsHex("|-|".join(vulns).encode()) db.insert_css_score(team, image, score, vulns) + ips[team] = request.remote_addr return("OK") else: print("[ERROR] Vuln data decryption failed.") return("FAIL") return("FAIL") +@app.route('/scores/css/new', methods=['POST']) +def css_new_team(): + em.load() + request_ip = request.remote_addr + if request_ip != permitted_new_css_ip: + print("[ERROR] New team ID request wasn't from the authorized IP.") + return("FAIL") + try: + team = request.form["team"].rstrip().strip() + id = request.form["id"].rstrip() + if "email" in request.form: + email = request.form["email"] + else: + email = None + except: + print("[ERROR] New team ID request did not have all required fields (team, id).") + return("FAIL") + if not db.validate_alphanum(team) or not db.validate_alphanum(id): + print("[ERROR] New team or id contained illegal characters.") + return("FAIL") + config = db.read_running_config() + try: + if team in config["remote"]["team_aliases"]: + print("[ERROR] Duplicate team alias (team)") + return("FAIL") + except: + pass + try: + if id in config["remote"]["teams"]: + print("[ERROR] Duplicate team id (id)") + return("FAIL") + except: + pass + try: + if email in config["remote"]["team_emails"]: + print("[ERROR] Duplicate team email (email)") + return("FAIL") + except: + pass + if not "remote" in config: + config["remote"] = {} + if email and not db.validate_email(email): + print("[ERROR] Email was not null and contained illegal characters.") + return("FAIL") + else: + if "team_emails" in config["remote"]: + config["remote"]["team_emails"].append(email) + else: + config["remote"]["team_emails"] = [email] + if len(config["remote"]["team_emails"]) <= len(config["remote"]["teams"]): + for x in range(len(config["remote"]["teams"]) - len(config["remote"]["team_emails"]) + 1): + config["remote"]["team_emails"].insert(0, "N/A") + if "teams" in config["remote"]: + config["remote"]["teams"].append(id) + else: + config["remote"]["teams"] = [id] + if "team_aliases" in config["remote"]: + config["remote"]["team_aliases"].append(team) + else: + config["remote"]["team_aliases"] = [team] + print(config) + db.write_running_config(config) + return("OK") + @app.route('/scores/css/status') def css_status(): return("OK") @@ -306,7 +376,7 @@ def css_status(): def css_csv(): em.load() csv_buffer = BytesIO() - csv_buffer.write(db.get_css_csv(em.remote)) + csv_buffer.write(db.get_css_csv(em.remote, ips)) csv_buffer.seek(0) return send_file(csv_buffer, as_attachment=True, attachment_filename='score_report.csv',