···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}');