···11-from flask import Flask, render_template, request, redirect, url_for, session, make_response
22-import time
33-import secrets
44-import json
55-66-77-app = Flask(__name__)
88-app.secret_key = secrets.token_hex(16)
99-1010-user_db = {}
1111-""" format ->
1212- username: "password"
1313- }
1414-"""
1515-1616-request_rates = {}
1717-""" format ->
1818- "ip_addr":{
1919- "num_requests": int
2020- "epoch_start": timestamp
2121- "lockout_until" : int # -1 if not locked out, timestamp of lockout end
2222- }
2323-"""
2424-2525-MAX_REQUESTS = 10 # max failed attempts before a user is locked out
2626-EPOCH_DURATION = 30 # timeframe for failed attempts (in seconds)
2727-LOCKOUT_DURATION = 120 # duration a user will be locked out for (in seconds)
2828-2929-RATE_LIMITED_HTML = "<h1>Rate Limited Exceeded</h1><p>You have sent too many requests, requests from your IP will be temporarily blocked.</p>"
3030-3131-3232-3333-## ------------------------ HELPER FUNCTIONS ------------------------ ##
3434-3535-"""Quick function to no-cache web page responses"""
3636-def no_cache(response):
3737- response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
3838- response.headers["Pragma"] = "no-cache"
3939- response.headers["Expires"] = "-1"
4040- return response
4141-4242-4343-"""Returns true if a user is logged in, false otherwise"""
4444-def logged_in():
4545- if "user" in session:
4646- return True
4747- return False
4848-4949-5050-"""Returns the current user (or None if there is none)"""
5151-def current_user():
5252- if "user" in session:
5353- return session["user"]
5454- return None
5555-5656-5757-"""Add a new user to db"""
5858-def add_new_user(username, password):
5959- user_db[username] = password
6060- print("Added (username=%s, password=%s) to user_db" % (username, password))
6161-6262-6363-""" Updates the request rates db for a given client ip, since information will likely be stale."""
6464-def refresh_request_rates_db(client_ip):
6565- curr_time = time.time()
6666- if client_ip not in request_rates:
6767- return
6868-6969- # check if attempt interval has elapsed, if so sets it to 0
7070- epoch_start_time = request_rates[client_ip]["epoch_start"]
7171- if curr_time - epoch_start_time > EPOCH_DURATION:
7272- request_rates[client_ip]["num_requests"] = 0
7373- request_rates[client_ip]["epoch_start"] = -1
7474-7575- # if was locked out but period ended update store
7676- lockout_end = request_rates[client_ip]["lockout_until"]
7777- if (lockout_end != -1) and time.time() >= lockout_end:
7878- request_rates[client_ip]["lockout_until"] = -1
7979-8080-8181-"""For a given user IP, checks how many requests the user has made (by updating the storage) and if
8282-the user it has exceeded the assigned rate limit. Returns true if the user has exceeded rate limit,
8383-false otherwise. """
8484-def exceeded_rate_limit() -> bool: # Could do a daemon, but since checks of status are always done before updating its not really necessary
8585- curr_time = time.time()
8686-8787- # Grab the IP of the client
8888- client_ip = request.remote_addr
8989- print(f"Request ip address: {client_ip}", flush=True)
9090-9191- # refresh & add new entry to db if it doesnt exist
9292- refresh_request_rates_db(client_ip)
9393- if client_ip not in request_rates:
9494- request_rates[client_ip] = {
9595- "num_requests": 0,
9696- "epoch_start": -1,
9797- "lockout_until": -1
9898- }
9999- print(f"New entry added to db", flush=True)
100100-101101- # log request if it was a POST
102102- if request.method == "POST":
103103- request_rates[client_ip]['num_requests'] += 1
104104- # if epoch hasnt started, set epoch
105105- if request_rates[client_ip]['epoch_start'] == -1:
106106- request_rates[client_ip]['epoch_start'] = curr_time
107107- print(f"DB updated - {client_ip}:{request_rates[client_ip]}", flush=True)
108108-109109- # check if we exceeded rate threshold, return True if so
110110- if request_rates[client_ip]['num_requests'] > MAX_REQUESTS:
111111- if request_rates[client_ip]["lockout_until"] == -1:
112112- request_rates[client_ip]['lockout_until'] = curr_time + LOCKOUT_DURATION
113113- print("Account locked out")
114114- print(f"DB - {client_ip}:{request_rates[client_ip]}", flush=True)
115115- return True
116116-117117- return False
118118-119119-120120-## ------------------------ APP ROUTES ------------------------ ##
121121-122122-""" Login portal """
123123-@app.route("/login", methods=['GET', 'POST'])
124124-def login():
125125- ## TODO - check rate limit
126126- if exceeded_rate_limit():
127127- return RATE_LIMITED_HTML
128128-129129- # if POST, accept form data and try to add user
130130- if request.method == "POST":
131131- user_input = request.form['username']
132132- pswd_input = request.form['password']
133133- print("User input: %s, password input: %s" % (user_input, pswd_input))
134134-135135- # non-existent user or bad password
136136- if (user_input not in user_db) or (user_db[user_input] != pswd_input):
137137- msg = f"Invalid username or password."
138138- return render_template("login.html", error=msg)
139139-140140- # authenticate user
141141- session["user"] = user_input
142142- print("Successfully logged in, session=%s" % (session))
143143- return redirect(url_for("index")) # note 'index' refers to the FUNCTION NAME
144144-145145- # return normal page if 'GET'
146146- return no_cache(make_response(render_template('login.html')))
147147-148148-149149-""" Homepage """
150150-@app.route("/", methods=['GET'])
151151-def index():
152152- if exceeded_rate_limit():
153153- return RATE_LIMITED_HTML
154154-155155- # authenticate
156156- if not logged_in():
157157- return redirect(url_for("login"))
158158-159159- # display homepage according to login
160160- user = current_user()
161161- flag = open("/challenge/flag.txt").read().strip()
162162- return no_cache(make_response(render_template("index.html", user=user, flag=flag)))
163163-164164-165165-""" Logout """
166166-@app.route("/logout", methods=['GET'])
167167-def logout():
168168- if exceeded_rate_limit():
169169- return RATE_LIMITED_HTML
170170-171171- if "user" in session:
172172- session.pop('user', None)
173173- print("Logged out, popped session")
174174- return redirect(url_for("login"))
175175-176176-177177-if __name__ == '__main__':
178178- username, password = None, None
179179- # get profile data
180180- try:
181181- with open("/challenge/profile.json", "r") as file:
182182- profile = json.load(file)
183183- username = profile["username"]
184184- password = profile["password"]
185185- except Exception as e:
186186- print(f"Error setting up profile in app:\n{e}")
187187- exit(1)
188188-189189- # add new user
190190- add_new_user(username, password)
191191-192192- # start app
193193- app.run(host='0.0.0.0', port=8000, debug=True)
···11-FROM node:18@sha256:5381bf8dd7e1dc53350b921f02811bd9167b34d3f7d0d8ebcc28229f65aef035 AS base
22-33-# Set up challenge directory
44-WORKDIR /challenge
55-66-# install packages
77-RUN npm install --no-package-lock \
88- express \
99- ejs \
1010- dotenv \
1111- pg \
1212- knex
1313-1414-# Copy only the source code
1515-COPY src/ ./src/
1616-1717-# Specify a new stage for the challenge. Everything previous to this can be
1818-# reused for every instance.
1919-FROM base AS challenge
2020-# Bring in FLAG from cmgr. Busts the cache every time.
2121-ARG FLAG
2222-2323-# Open up this port for the web server
2424-EXPOSE 80
2525-2626-CMD node /challenge/src/server.js
2727-2828-
···11-CREATE EXTENSION IF NOT EXISTS pgcrypto;
22-33-CREATE TABLE users (
44- id text PRIMARY KEY DEFAULT gen_random_uuid(),
55- username text NOT NULL,
66- password text NOT NULL,
77- created_at timestamptz NOT NULL DEFAULT now()
88-);
99-1010-CREATE TABLE tokens (
1111- id text PRIMARY KEY DEFAULT gen_random_uuid(),
1212- user_id text NOT NULL REFERENCES users(id),
1313- created_at timestamptz NOT NULL DEFAULT now(),
1414- expired_at timestamptz NOT NULL DEFAULT now() + interval '1 days'
1515-);
1616-1717-CREATE TABLE secrets (
1818- id text PRIMARY KEY DEFAULT gen_random_uuid(),
1919- owner_id text NOT NULL REFERENCES users(id),
2020- content text NOT NULL,
2121- created_at timestamptz NOT NULL DEFAULT now()
2222-);
2323-2424-2525-INSERT INTO users(id, username, password) VALUES ('e2a66f7d-2ce6-4861-b4aa-be8e069601cb', 'admin', 'fake_password');
2626-INSERT INTO secrets(owner_id, content) VALUES ('e2a66f7d-2ce6-4861-b4aa-be8e069601cb', 'picoCTF{fake_flag}');