Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
4db9035
Add files via upload
TrishaSrikanth-459 Feb 13, 2026
62d628e
Delete 75fcc8f2-a6eb-427c-b2b4-c68421f8cd36_ExportBlock-b6b6c766-31fd…
TrishaSrikanth-459 Feb 13, 2026
25cc8ab
Add files via upload
TrishaSrikanth-459 Feb 13, 2026
e8f2db9
Fix image link encoding in Writeup #1
TrishaSrikanth-459 Feb 13, 2026
a7b2c34
Delete ExportBlock-b6b6c766-31fd-405d-8856-91cd8a5dc230-Part-1 directory
TrishaSrikanth-459 Mar 3, 2026
085e849
Add files via upload
TrishaSrikanth-459 Mar 3, 2026
6bbf626
Update Writeup #1 2f71c9147015804582a8d45dae5f41b6.md
TrishaSrikanth-459 Mar 3, 2026
c0542e8
Rename Writeup #1 2f71c9147015804582a8d45dae5f41b6.md to Writeup#1.md
TrishaSrikanth-459 Mar 3, 2026
5c82dbb
Rename Screenshot_2026-01-29_at_6.09.20_PM.png to Contact-Us.png
TrishaSrikanth-459 Mar 3, 2026
90c170e
Rename Screenshot_2026-01-29_at_6.15.59_PM.png to Session-Hijacking.png
TrishaSrikanth-459 Mar 3, 2026
e00af3a
Rename Session-Hijacking.png to XSS-Attack.png
TrishaSrikanth-459 Mar 3, 2026
23c174f
Rename Screenshot_2026-02-01_at_7.12.22_PM.png to Session-Hijacking.png
TrishaSrikanth-459 Mar 3, 2026
a31f050
Delete ExportBlock-6993437b-eb91-4735-885c-d861beab598a-Part-1/Writeu…
TrishaSrikanth-459 Mar 3, 2026
8880d33
Add files via upload
TrishaSrikanth-459 Mar 3, 2026
afc7080
Rename Screenshot_2026-02-01_at_7.12.22_PM.png to Session-Hijacking.png
TrishaSrikanth-459 Mar 3, 2026
9664d12
Rename 1_tZaHIrqrHRwd2Bf5TvspUA.webp to Chat-With-Admin.webp
TrishaSrikanth-459 Mar 3, 2026
2298183
Rename 1_DhdmTHv0CTUsf0Xi67dulA.webp to Privilege-Escalation.webp
TrishaSrikanth-459 Mar 3, 2026
ea5544e
Rename Privilege-Escalation.webp to Finance-Webpage.webp
TrishaSrikanth-459 Mar 3, 2026
fa103cb
Rename Screenshot_2026-02-01_at_7.36.36_PM.png to Privilege-Esalation…
TrishaSrikanth-459 Mar 3, 2026
773e28b
Update Writeup#1.md
TrishaSrikanth-459 Mar 3, 2026
6b53538
Create Writeup-#1
TrishaSrikanth-459 Mar 3, 2026
3312d47
Rename Chat-With-Admin.webp to Chat-With-Admin.webp
TrishaSrikanth-459 Mar 3, 2026
16ab9be
Rename Contact-Us.png to Contact-Us.png
TrishaSrikanth-459 Mar 3, 2026
9ae8b01
Rename XSS-Attack.png to XSS-Attack.png
TrishaSrikanth-459 Mar 3, 2026
556ded2
Rename Session-Hijacking.png to Session-Hijacking.png
TrishaSrikanth-459 Mar 3, 2026
2bcf602
Rename Privilege-Esalation.png to Privilege-Esalation.png
TrishaSrikanth-459 Mar 3, 2026
d2ced30
Rename Finance-Webpage.webp to Finance-Webpage.webp
TrishaSrikanth-459 Mar 3, 2026
047f7f3
Delete ExportBlock-6993437b-eb91-4735-885c-d861beab598a-Part-1/Writeu…
TrishaSrikanth-459 Mar 3, 2026
015642f
Update Writeup#1.md
TrishaSrikanth-459 Mar 3, 2026
2562c0a
Rename Privilege-Esalation.png to Privilege-Escalation.png
TrishaSrikanth-459 Mar 3, 2026
d72fc9c
Add files via upload
TrishaSrikanth-459 Mar 15, 2026
dfa4059
Delete writeup.md
TrishaSrikanth-459 Mar 15, 2026
3087e17
Add files via upload
TrishaSrikanth-459 Mar 15, 2026
c1be2f0
Delete spiky_tamagotchi_vulnerability_report.md
TrishaSrikanth-459 Mar 15, 2026
5c5a2f0
Add files via upload
TrishaSrikanth-459 Apr 13, 2026
cbef07d
Delete vulnerability_report.md
TrishaSrikanth-459 Apr 13, 2026
17f45ae
Rename Writeup#1.md to TryHackMe_Web_Application_Red_Teaming.md
TrishaSrikanth-459 Apr 26, 2026
6fd3763
Rename writeup to TryHackMe/Web Application Red Teaming
TrishaSrikanth-459 Apr 26, 2026
94abb9f
Rename Chat-With-Admin.webp to Chat-With-Admin.webp
TrishaSrikanth-459 Apr 26, 2026
313067e
Rename Contact-Us.png to Contact-Us.png
TrishaSrikanth-459 Apr 26, 2026
0cf3b7a
Rename Finance-Webpage.webp to Finance-Webpage.webp
TrishaSrikanth-459 Apr 26, 2026
38ab888
Rename Privilege-Escalation.png to Privilege-Escalation.png
TrishaSrikanth-459 Apr 26, 2026
e03d1da
Rename Session-Hijacking.png to Session-Hijacking.png
TrishaSrikanth-459 Apr 26, 2026
6a8db08
Rename XSS-Attack.png to XSS-Attack.png
TrishaSrikanth-459 Apr 26, 2026
6ed4fe6
Rename TryHackMe_Web_Application_Red_Teaming.md to web-application-re…
TrishaSrikanth-459 Apr 26, 2026
baf2815
Rename XSS-Attack.png to XSS-Attack.png
TrishaSrikanth-459 Apr 26, 2026
d44e6e6
Rename Session-Hijacking.png to Session-Hijacking.png
TrishaSrikanth-459 Apr 26, 2026
decaffb
Rename Privilege-Escalation.png to Privilege-Escalation.png
TrishaSrikanth-459 Apr 26, 2026
8c90a8c
Rename Finance-Webpage.webp to Finance-Webpage.webp
TrishaSrikanth-459 Apr 26, 2026
d9da4f2
Rename Contact-Us.png to Contact-Us.png
TrishaSrikanth-459 Apr 26, 2026
12098f5
Rename web-application-red-teaminTryHackMe_Web_Application_Red_Teamin…
TrishaSrikanth-459 Apr 26, 2026
200694d
Rename Chat-With-Admin.webp to Chat-With-Admin.webp
TrishaSrikanth-459 Apr 26, 2026
35dab65
Add files via upload
TrishaSrikanth-459 May 5, 2026
f77da44
Rename RelayBoardCTF/Dockerfile to RelayBoardCTF/backend/Dockerfile
TrishaSrikanth-459 May 5, 2026
ee39e53
Rename RelayBoardCTF/docker-compose.yml to RelayBoardCTF/backend/dock…
TrishaSrikanth-459 May 5, 2026
ac76607
Update README.md
TrishaSrikanth-459 May 5, 2026
c94be0e
Update Vulnerability_Report.md
TrishaSrikanth-459 May 5, 2026
eb63e21
Delete RelayBoardCTF/backend/__pycache__ directory
TrishaSrikanth-459 May 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions RelayBoardCTF/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# RelayBoard CTF

RelayBoard is an original easy-medium web application CTF built around a shift
handoff portal for operations teams.

## What Is Included

- `backend/`: all vulnerable backend code, templates, static assets, and snippet files
- `solve.py`: proof-of-concept exploit script
- `Vulnerability_Report.md`: formal report
- `Dockerfile`: container entrypoint for public deployment
- `docker-compose.yml`: simple public-server launch configuration

## Goal

Recover the flag stored in the private admin packet.

## Live Challenge URL

Players can access the challenge here:

`https://hung-concessible-overjoyfully.ngrok-free.dev`

## Intended Solve Path

1. Register a normal user account.
2. Abuse the packet preview include resolver to read `backend/config.py`.
3. Recover the Flask secret key from the source.
4. Forge an admin session cookie.
5. Open `/admin/archive/1` and capture the flag.
96 changes: 96 additions & 0 deletions RelayBoardCTF/Vulnerability_Report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Vulnerability Report

## Context

RelayBoard is a Flask-based web application intended for operations teams to
prepare and review shift handoff packets. It is designed to run as a Linux
userspace web service, stores data in a local SQLite database, and is shipped
in this project as a modular backend package under
`RelayBoardCTF/backend/`. The application exposes
routes for registration, login, composing packet drafts, previewing draft
content, and reviewing saved packets.

The packet preview feature is relevant to the challenge. Operators can draft a
packet and preview how shared snippet references render before saving. The UI
documents a snippet syntax such as `[[include:ops-footer.txt]]`, and the
backend replaces those directives with the contents of files from the local
`backend/snippets/` directory.

The program receives inputs over HTTP form posts. In particular, `/preview`
accepts the user-controlled `title`, `body`, and `checklist` fields and renders
them immediately. The challenge objective is to recover the flag stored in the
private admin packet by exploiting the preview functionality. A proof-of-
concept exploit is provided in `RelayBoardCTF/solve.py`.

## Vulnerability

The primary issue is a path traversal vulnerability, corresponding to CWE-22:
Improper Limitation of a Pathname to a Restricted Directory. A secondary design
weakness makes the impact worse: the Flask session secret is recoverable from
the application source, allowing the attacker to forge a privileged cookie once
an arbitrary file read is achieved.

The vulnerability manifests in `render_handoff_preview()` in
`RelayBoardCTF/backend/preview.py`, where the
preview engine processes `[[include:...]]` directives. The `replace_include()`
helper concatenates the user-supplied snippet name directly onto
`SNIPPET_DIR` and calls `read_text()` without normalizing or constraining the
resolved path.

Because the `/preview` route passes attacker-controlled form fields directly
into `render_handoff_preview()` in
`RelayBoardCTF/backend/routes.py`, an
authenticated low-privilege user can submit inputs such as
`[[include:../config.py]]` and force the server to read files outside the
intended snippet directory. Reading `backend/config.py` discloses the default
Flask `SECRET_KEY` value, which is then used by the exploit script to mint a
new cookie containing `role=admin`. The administrative authorization check that
trusts this cookie is implemented in
`RelayBoardCTF/backend/auth.py`.

## Exploitation

The overall exploitation path is:

1. Create a normal account through `/register`.
2. Submit a crafted preview request to `/preview` with `body=[[include:../app.py]]`.
3. Parse the returned source code to recover the Flask secret key.
4. Forge a signed Flask session cookie containing admin role data.
5. Request `/admin/archive/1` and read the flag from the private admin packet.

The first exploit primitive is arbitrary file disclosure relative to the
application directory. This breaks the intended trust boundary around the
`snippets/` directory and exposes source code and configuration material.

The second exploit primitive is authenticated session forgery. Because the
application trusts the client-side Flask session for authorization decisions,
knowledge of the secret key is sufficient to create a cookie that passes the
`admin_required` check. The proof-of-concept exploit in
`RelayBoardCTF/solve.py` recovers the secret from
the preview response, forges a valid cookie, installs it into the session jar,
and then requests the admin-only archive route.

Together, these primitives allow a low-privilege attacker to move from an
ordinary user account to full administrative read access and recover the flag.

## Remediation

The preview include resolver should be constrained to an allowlisted snippet
directory. A safe patch would resolve the requested path, reject absolute paths
and traversal components, and verify that the final path remains under
`SNIPPET_DIR` before opening it. If nested snippet folders are needed, the
application should still compare the resolved path prefix against the snippet
root after normalization.

The application should also stop relying on client-controlled role claims for
authorization. The server can store sessions server-side, or at minimum load
fresh authorization data from the database on each request rather than trusting
the cookie's `role` field. In addition, the Flask secret must not be hardcoded
in source. It should be injected from an environment variable or dedicated
secret store, and rotated if disclosure is suspected.

Variant analysis for similar issues can be automated by searching for code that
joins attacker-controlled path fragments onto filesystem roots and then opens
the result without a resolved-path containment check. A second review pass
should identify any logic that grants permissions directly from client-signed
session contents instead of server-side authorization state.
13 changes: 13 additions & 0 deletions RelayBoardCTF/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

ENV PORT=5000
EXPOSE 5000

CMD ["sh", "-c", "gunicorn --bind 0.0.0.0:${PORT:-5000} backend.wsgi:app"]
20 changes: 20 additions & 0 deletions RelayBoardCTF/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from flask import Flask

from .auth import auth_bp
from .config import Config
from .db import close_db, init_db
from .routes import main_bp


def create_app():
app = Flask(__name__, template_folder="templates", static_folder="static")
app.config.from_object(Config)

app.register_blueprint(auth_bp)
app.register_blueprint(main_bp)
app.teardown_appcontext(close_db)

with app.app_context():
init_db()

return app
9 changes: 9 additions & 0 deletions RelayBoardCTF/backend/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import os

from .wsgi import app


if __name__ == "__main__":
host = os.environ.get("HOST", "0.0.0.0")
port = int(os.environ.get("PORT", "5000"))
app.run(host=host, port=port, debug=False)
101 changes: 101 additions & 0 deletions RelayBoardCTF/backend/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import sqlite3
from functools import wraps

from flask import Blueprint, abort, g, redirect, render_template, request, session, url_for
from werkzeug.security import check_password_hash, generate_password_hash

from .db import get_db


auth_bp = Blueprint("auth", __name__)


def current_user():
user_id = session.get("user_id")
if user_id is None:
return None
return get_db().execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()


def login_required(view):
@wraps(view)
def wrapped_view(**kwargs):
if current_user() is None:
return redirect(url_for("auth.login"))
return view(**kwargs)

return wrapped_view


def admin_required(view):
@wraps(view)
def wrapped_view(**kwargs):
if session.get("role") != "admin":
abort(403)
return view(**kwargs)

return wrapped_view


@auth_bp.before_app_request
def load_logged_in_user():
g.user = current_user()


@auth_bp.route("/register", methods=["GET", "POST"])
def register():
error = None
if request.method == "POST":
username = request.form.get("username", "").strip()
password = request.form.get("password", "")

if not username or not password:
error = "Username and password are required."
else:
db = get_db()
try:
db.execute(
"INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)",
(username, generate_password_hash(password), "user"),
)
db.commit()
user = db.execute(
"SELECT * FROM users WHERE username = ?",
(username,),
).fetchone()
session.clear()
session["user_id"] = user["id"]
session["username"] = user["username"]
session["role"] = user["role"]
return redirect(url_for("main.index"))
except sqlite3.IntegrityError:
error = "That username is already taken."
return render_template("register.html", error=error)


@auth_bp.route("/login", methods=["GET", "POST"])
def login():
error = None
if request.method == "POST":
username = request.form.get("username", "").strip()
password = request.form.get("password", "")
user = get_db().execute(
"SELECT * FROM users WHERE username = ?",
(username,),
).fetchone()

if user is None or not check_password_hash(user["password_hash"], password):
error = "Invalid credentials."
else:
session.clear()
session["user_id"] = user["id"]
session["username"] = user["username"]
session["role"] = user["role"]
return redirect(url_for("main.index"))
return render_template("login.html", error=error)


@auth_bp.route("/logout")
def logout():
session.clear()
return redirect(url_for("main.index"))
19 changes: 19 additions & 0 deletions RelayBoardCTF/backend/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import os
from pathlib import Path


PACKAGE_ROOT = Path(__file__).resolve().parent
PROJECT_ROOT = PACKAGE_ROOT.parent


class Config:
SECRET_KEY = os.environ.get(
"RELAYBOARD_SECRET",
"relayboard-dev-secret-please-rotate",
)
DB_PATH = PROJECT_ROOT / "relayboard.db"
SNIPPET_DIR = PACKAGE_ROOT / "snippets"
FLAG_VALUE = os.environ.get(
"RELAYBOARD_FLAG",
"flag{night_shift_packets_need_real_boundaries}",
)
Loading