Challenge: Heart Part 7 · CTF: DalCTF 2026 · Category: Web · Difficulty: Hard
TL;DR
A Kendrick-Lamar-themed Flask app chained three classic bugs into one flag:
- SQL injection in the public scroll search leaks the admin password.
- Logging in unlocks the “Master’s Chamber” admin panel exposing an internal M.A.A.D Cipher microservice.
- That service has a Heartbleed-style buffer over-read (
{data, size}) that bleeds the AES-256 master key straight out of process memory.
Decrypt the flag ciphertext with the leaked key:
| |
The challenge name ("Heart Part 7") and the flavor text ("what happens on earth stays on earth" — i.e. memory you weren’t supposed to see) are both giant winks at the intended vuln: Heartbleed (CVE-2014-0160).
Recon
The landing page is a dojo run by “Master Kendrick,” with a navbar pointing at /, /search, and /login. The server header gives the stack away immediately:
| |
So we’re looking at a Flask app. One line on the homepage is worth circling:
Every technique is documented, and the most sacred knowledge is kept sealed by the M.A.A.D Cipher.
“Sealed by a cipher” + a members-only portal screams “there’s an encrypted flag and a key hidden somewhere.” Two interactive surfaces stand out:
/search?q=— a public technique search./login— a username/password members portal.
Step 1 — SQL Injection in /search
The search form is a plain GET that reflects your query and lists matching technique names. First, baseline behaviour:
| |
Now the universal first probe — a single quote:
| |
No 500, no error… but also no crash, which already hints the quote is being swallowed inside a string context. Time for a boolean test:
| |
That’s a textbook injectable WHERE clause. The OR '1'='1 makes the predicate always true (dumps everything); prefixing a non-matching term keeps both sides false. SQLi confirmed.
Next, find the column count for a UNION. The results table has two columns (Technique / Description), so:
| |
renders a row with 1 and 2 — two columns, both reflected. Game over for the database.
Fingerprinting + schema dump
| |
SQLite. Pull the whole schema from sqlite_master (using replace(sql, char(10), ' ') to flatten newlines so they survive the HTML table):
| |
| |
Three tables, two of them juicy: users (creds!) and scrolls (the sealed content_encrypted + iv).
Dumping the goods
| |
| |
And the sealed scroll:
| |
| |
So we have AES-shaped base64 blobs, but no key. Hold that thought — first, let’s use those admin creds.
Admin creds:
admin/duckworth(a nod to Kendrick Lamar Duckworth).
Step 2 — The Master’s Chamber
Logging in at /login issues a JWT-looking session cookie and 302-redirects to /admin:
| |
/admin — the “Master’s Chamber” — exposes three things via inline JS:
- Get Encrypted Flag →
GET /api/flag - Check Cipher Health →
POST /cipher/health - Manage Techniques → CRUD under
/api/techniques
Pull the flag blob:
| |
| |
AES-256-CBC, 32 bytes of ciphertext (two blocks), a fresh IV — and still no key. The key has to be hiding in the M.A.A.D Cipher service. Look at the health check the page sends:
| |
| |
UElORw== decodes to PING. So the service echoes your data back, base64-encoded. But notice it takes a separate size field alongside the data…
data+ a separatesizefield. A length you control, independent of the payload length. If that’s stop-sounding-familiar, the challenge title is literally “Heart”.
Step 3 — Heartbleed
Heartbleed (CVE-2014-0160) is a buffer over-read: the client says “here are N bytes, echo back M bytes”, and a server that trusts M without checking it against N happily copies M bytes out of a buffer — returning whatever adjacent heap memory was sitting after your N bytes.
Here the parallel is exact: we send data (the payload) and size (how much to echo). Let’s ask for more than we send:
| |
| |
At size=1000 the response clamps to 512 bytes — and that buffer is full of somebody else’s memory:
| |
There it is: KENDRICK_MASTER_KEY= followed by exactly 32 raw bytes — the AES-256 key — bled straight out of the cipher service’s heap. What happens on earth stays on earth… except when it leaks over the wire.
Step 4 — Decrypt the Flag
Carve the 32 bytes that follow the KENDRICK_MASTER_KEY= marker, then run a standard AES-256-CBC decrypt with the IV from /api/flag:
| |
| |
Strip the PKCS#7 padding (\x08 × 8) and we’re done:
| |
Why It Worked — The Chain
No single bug handed over the flag; the difficulty is in stitching them:
| # | Vuln | What it gave us |
|---|---|---|
| 1 | UNION-based SQLi in /search | Admin password duckworth + the encrypted flag location |
| 2 | Authenticated admin panel | The /api/flag ciphertext and the internal cipher service |
| 3 | Heartbleed over-read in /cipher/health | The AES-256 master key from heap memory |
| 4 | AES-256-CBC decrypt | The flag |
The encryption was never the weak point — AES-256-CBC is fine. The weak point was trusting a length field, the exact mistake that made the original Heartbleed so devastating.
Remediation
- SQLi: use parameterized queries / bound placeholders, never string-concatenate user input into SQL.
- Heartbleed: validate
size <= len(data)(or just drop the field entirely and echolen(data)). Never let a client-supplied length drive a memory copy. - Defense in depth: don’t keep long-lived secrets (master keys) in the same process memory exposed by a debug/health echo; and don’t store recoverable plaintext admin passwords.
Lessons / Takeaways
- A health/echo endpoint with a separate size parameter is a Heartbleed tell — especially when the challenge is literally named “Heart.”
- Read the flavor text. “what happens on earth stays on earth” = memory disclosure foreshadowing.
- Always over-read echo endpoints with
size > len(payload)and inspect the trailing bytes forKEY=,SECRET=, tokens, or other heap residue.
Sit down, be humble. 🥋