Writeups/TryHackMe/Valenfind - TryHackMe Writeup | Path Traversal to Admin API
TryHackMeMediumRoom

Valenfind - TryHackMe Writeup | Path Traversal to Admin API

Step-by-step Valenfind TryHackMe writeup from Love At First Breach CTF 2026: LFI path traversal, Flask app source leak, and admin API key leading to the flag.

##TryHackMe Room — Valenfind (Love At First Breach 2026)

Can you find vulnerabilities in this new dating app?

Valenfind is a Valentine-themed dating app from TryHackMe's Love At First Breach 2026 event. The challenge description explicitly hints that the creator "only learned to code this year" and that the app might be "vibe-coded" — which to me was a strong signal to look for classic web misconfigurations rather than obscure logic bugs. My goal was to find a way in, understand the application's attack surface, and ultimately retrieve the flag. This writeup documents my full path from reconnaissance to flag, with each step connected so you can follow the same workflow and understand why each move led to the next.


##Overview

ItemDetail
GoalFind and exploit vulnerabilities to retrieve the flag
Attack chainPath traversal (LFI) → source code leak → admin API key → database export → flag
Key conceptsLocal File Inclusion (LFI), path traversal, Flask session, secret API

##Reconnaissance

The application was reachable at http://<TARGET_IP>:5000. I started by creating a temporary account so I could explore the full user flow: dashboard, profiles, and any API calls the frontend might make. I had Burp Suite running in the background with the browser proxied through it, so every request and response would be visible. My aim was to spot parameters that looked like they referenced server-side resources — for example file names, template names, or paths — since those are common places for path traversal or inclusion bugs.

Valenfind homepage
Valenfind homepage

###Finding a suspicious endpoint

While browsing another user's profile (e.g. clicking through from the dashboard), I noticed in Burp's HTTP history a request that stood out:

http
GET /api/fetch_layout?layout=theme_classic.html HTTP/1.1 Host: <TARGET_IP>:5000 Accept: */* Referer: http://<TARGET_IP>:5000/profile/romeo_montague Cookie: session=...

The server responded with 200 OK and a body that was clearly an HTML fragment — a small block containing placeholders like __USERNAME__ and __BIO__. So the backend was using the layout parameter to choose which template or component file to load and return its contents. That meant user input was being fed into something that built a file path. If the application did not properly sanitize or restrict that parameter, we might be able to break out of the intended directory and read other files (e.g. /etc/passwd, configuration files, or — most usefully — the application's own source code). I decided to test this hypothesis by trying a classic path traversal payload.


##Key concept: Path traversal (LFI)

Local File Inclusion (LFI) occurs when user-controlled input is used to construct a path that the server then uses to read (or include/execute) files from disk. If the application does not normalize the path, reject ../ sequences, or restrict access to a strict allowlist, an attacker can use sequences like ../../../../../etc/passwd to "escape" the intended directory and read arbitrary files. In web applications, this often appears in parameters named things like file, page, template, or — as here — layout. The impact can range from reading sensitive config files to leaking source code (and thus secrets, logic, or further attack surface). Here, the response was being returned directly to the client, so at minimum we had a read primitive; the next step was to confirm it and then use it to pull the application source.


##Exploiting path traversal

I sent a request with a traversal payload in the layout parameter, trying to reach /etc/passwd from a typical web app base path:

http
GET /api/fetch_layout?layout=../../../../../etc/passwd HTTP/1.1 Host: <TARGET_IP>:5000 Cookie: session=...

Response (snippet):

text
HTTP/1.1 200 OK Server: Werkzeug/3.0.1 Python/3.12.3 Content-Type: text/html; charset=utf-8 root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin ... www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin ... ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash ...

So the server was indeed reading arbitrary files and returning their contents in the response. That confirmed a full path traversal / LFI. I also tried sending a path that was clearly a directory (e.g. ending in ../../../../../) to see what error the application would return; sometimes error messages leak the base path. The response was something like:

text
Error loading theme layout: [Errno 21] Is a directory: '/opt/Valenfind/templates/components/../../../../../'

From that I learned the application was running from /opt/Valenfind/ and that the layout parameter was being appended to a base path under templates/components/. So we were dealing with a Flask (or similar Python) application, and the main entry point would likely be something like app.py in the project root. The next logical step was to try to read that file so we could see routes, secrets, and any other interesting logic.

Path traversal to /etc/passwd
Path traversal to /etc/passwd


##Leaking application source code

I adjusted the traversal to go up from templates/components/ to the project root and then reference the main application file:

http
GET /api/fetch_layout?layout=../../app.py HTTP/1.1 Host: <TARGET_IP>:5000 Cookie: session=...

The server returned 200 with the full contents of the Flask application. I went through the source carefully. Two things stood out immediately:

  1. >

    Admin API endpoint — There was a route /api/admin/export_db that sent the SQLite database file as a download. Access was gated by a custom header: X-Valentine-Token had to match a hardcoded value (in the source it was something like ADMIN_API_KEY = "CUPID_MASTER_KEY_2024_XOXO" — I've obfuscated it here as <REDACTED> so the writeup doesn't leak the actual key). So if we could learn that value, we could call the endpoint and download the database, which would almost certainly contain the flag or data leading to it.

  2. >

    Blacklist bypass — The same route that served the layout had a small "security" check: it refused to serve paths that contained the strings cupid.db or seeder.py. So the developer had tried to block direct access to the database and the seeder script, but they did not block reading app.py. That meant the "secret" API key was sitting in a file we could read via the same LFI we had just used. There was no need for any further exploitation of the path traversal — we had everything we needed to call the admin export.

I copied the exact ADMIN_API_KEY value from the leaked source and prepared a request to the admin endpoint.


##Chaining to the flag: Admin export

Using the leaked API key in the required header, I sent:

http
GET /api/admin/export_db HTTP/1.1 Host: <TARGET_IP>:5000 X-Valentine-Token: <REDACTED> Cookie: session=...

The server responded with a 200 and the SQLite database file as the response body (or as an attachment, depending on how the client handles it). I saved it and opened it locally (e.g. with sqlite3 or a GUI tool), or simply searched the raw response for the flag format THM{...}. The challenge flag was present either inside a table or in a blob, and I had completed the chain: LFI → source leak → admin key → database export → flag.

Valenfind flag
Valenfind flag


##Flag

text
THM{*redacted*}

(Replace with the actual flag when you solve the room.)


##Attack chain summary

StepActionOutcome
1Intercept traffic and find /api/fetch_layout?layout=...Identify file-read endpoint
2Path traversal to /etc/passwdConfirm LFI
3Use error message / path logic to infer base pathLearn /opt/Valenfind/ and templates/components/
4Path traversal to ../../app.pyLeak Flask source and admin API key
5Request /api/admin/export_db with X-Valentine-TokenDownload DB and obtain flag

##Pitfalls and notes

  • >

    Blacklist vs allowlist: The application only blocked paths containing certain strings (cupid.db, seeder.py). That is a weak, blacklist-based approach. Reading app.py was allowed, so the entire security model (the "secret" admin key) collapsed. A safer approach would be to allow only a fixed list of layout filenames (allowlist) or to resolve the path and ensure it stays under a single directory (e.g. using os.path.normpath and checking the result).

  • >

    Trust in client-controlled paths: Building a file path directly from a query parameter without normalization or validation is a very common cause of path traversal. Once you see a parameter that looks like a filename or path, it's worth testing traversal payloads early.

  • >

    Storing secrets in source: Hardcoding an API key in the application source means that any bug allowing source code disclosure (here, LFI) also discloses the key. Secrets should be in environment variables or a secure secret store, and the application should never send them to the client.


##References and tools


This writeup is part of my Love At First Breach 2026 event writeups.

$ echo "Open to Red Team Security Research and Security Engineering roles."

> Open to Red Team Security Research and Security Engineering roles.

$ uptime

> Portfolio online since 2024 | Last updated: Mar 2026

"No one is useless in this world who lightens the burdens of another." — Charles Dickens

Considered a small donation if you found any of the walkthrough or blog posts helpful. Much appreciate :)

Buy me a coffee

© 2026 Shivang Tiwari. Built with Next.js. Hack the planet.