Old Sessions picoCTF 2026 Solution

Published: March 20, 2026

Description

Proper session timeout controls are critical for securing user accounts. If a user logs in on a public computer but doesn't log out, and session expiration dates are misconfigured, the session may remain active indefinitely, allowing an attacker to access the account without ever knowing the password.

This challenge wraps that idea around a debug endpoint that leaks every active session token in the database. Find the admin's stale token, replay it as a cookie, and the application authenticates you as admin.

Launch the challenge instance and open the web application in a browser.

Register an account or sign in with the test credentials so you can see what an authenticated session looks like.

Open DevTools, switch to the Application/Storage tab, and note the name of the session cookie the server sets after login (typically session, sessionid, or PHPSESSID).

bash
curl -c jar.txt -b jar.txt -d 'username=test&password=test' http://<HOST>:<PORT_FROM_INSTANCE>/login

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
Cookie-based session theft is the same primitive used in Cookie Monster Secret Recipe and is closely related to Fool the Lockout. For a deeper walkthrough of how cookies and JWTs hold sessions together, see the Cookie and JWT attacks for CTF post.
  1. Step 1
    Register, log in, and check /sessions
    Observation
    I noticed the challenge description mentioned a debug endpoint that leaks session tokens, which suggested that an authenticated request to /sessions would expose the admin's stale token directly without any password needed.
    Register a test account, log in, then navigate to /sessions. This page exposes all active session tokens in the database, including the admin's.
    bash
    # In the browser: register a new account, then log in
    bash
    # Then navigate to: http://<HOST>:<PORT_FROM_INSTANCE>/sessions
    bash
    # Or with curl:
    bash
    curl -c jar.txt -d 'username=test&password=test' http://<HOST>:<PORT_FROM_INSTANCE>/register
    bash
    curl -c jar.txt -b jar.txt -d 'username=test&password=test' http://<HOST>:<PORT_FROM_INSTANCE>/login
    bash
    curl -b jar.txt http://<HOST>:<PORT_FROM_INSTANCE>/sessions

    Expected output

    picoCTF{s3ss10n_t1m30ut_...}

    The vulnerable endpoint is /sessions. Instead of restricting it to the current user, it dumps every session stored in the Redis backend, one per numbered line. Each line is a key name (session:<id>) followed by the decoded session payload:

    1) session:PzkDYsNCGgrNQYyrVObF2UvaQvMp_RU-dAfk4rPEFyM, {'_permanent': True, 'key': 'test'}
    2) session:mL_C8lzVGrqTt1qiyIqiRnH0YyEPDqQMGEB-a_vrIsg, {'_permanent': True, 'key': 'admin'}

    The part after the colon and before the comma is the raw session ID. The key field in the payload tells you which user the session belongs to. Find the entry where 'key': 'admin' and copy the session ID.

    What didn't work first

    Tried: Running a directory brute-force with gobuster or ffuf before manually browsing the application

    gobuster will enumerate paths but the /sessions endpoint requires an authenticated session cookie to respond with token data rather than redirecting to /login. Hitting it unauthenticated returns a 302 or empty page. You must log in first, carry the session cookie in your requests, and only then will /sessions expose the full token dump.

    Tried: Looking for the admin token in the Set-Cookie header of the login response

    The server only issues a token for the account you just authenticated as, not for other users. The admin's stale token was created in a prior session and lives in the server-side Redis store. It is only visible at the /sessions listing endpoint, not in any HTTP response header from your own login flow.

    Learn more

    Session management is one of the most critical and most commonly misconfigured aspects of web application security. A session token is a secret value that identifies an authenticated user to the server after login. If session tokens are exposed or never invalidated, attackers can reuse them to authenticate as the original user without knowing their password.

    In a well-designed application, session tokens should be: randomly generated with sufficient entropy (at least 128 bits), transmitted only over HTTPS, stored in HttpOnly cookies to prevent JavaScript access, and invalidated on logout and after a configurable idle/absolute timeout. The OWASP Session Management Cheat Sheet documents these requirements in detail.

    This challenge demonstrates what happens when a developer accidentally exposes a session listing endpoint, a debug or admin feature left accessible in production. Always enumerate common paths like /api/sessions, /admin, /debug, and /.well-known/ during web application reconnaissance. Tools like ffuf, gobuster, and feroxbuster automate this with wordlists. For more on cookie-style session abuse see the Cookie and JWT attacks for CTF post; for the broader pattern catalogue see Web challenges and real-world bug patterns.

  2. Step 2
    Find the admin session
    Observation
    I noticed the /sessions response included a 'key' field in each session payload labeling the owning user, which suggested filtering for the entry where 'key' equals 'admin' would immediately identify the correct token without brute-forcing.
    The /sessions page labels each session by username ('key': 'admin'). Simply identify the row where the key is admin and copy that session ID. No brute-forcing is needed.
    bash
    # The /sessions response is plaintext: each line is 'session:<id>, {payload}'
    bash
    # Pull out session IDs and their key (username) with grep:
    bash
    curl -s http://<HOST>:<PORT_FROM_INSTANCE>/sessions | grep -oP "session:\K[^,]+(?=.*'key': 'admin')"
    bash
    # Or extract all IDs and probe which one reaches /admin:
    bash
    for tok in $(curl -s http://<HOST>:<PORT_FROM_INSTANCE>/sessions | grep -oP "session:\K[^,]+"); do
      echo -n "$tok -> "; curl -so /dev/null -w "%{http_code}\n" -b "session=$tok" http://<HOST>:<PORT_FROM_INSTANCE>/admin
    done
    What didn't work first

    Tried: Using grep with a pattern that matches the full 'session:ID' key including the prefix, then pasting that entire string as the cookie value

    The cookie value must be only the ID portion after 'session:' - the prefix is the Redis key name, not the token. Sending 'session=session:mL_C8lz...' causes the server to look up 'session:session:mL_C8lz...' in its store, which does not exist, so it treats the request as unauthenticated. Strip everything up to and including 'session:' before using the value.

    Tried: Brute-forcing the /admin route by cycling through all session IDs from the dump until one returns HTTP 200

    The /sessions dump already labels each token with its user via the 'key' field in the payload, so brute-forcing is unnecessary and slower. The grep one-liner with 'key': 'admin' pinpoints the correct token in one request. Brute-forcing also risks locking out or alerting the server if it logs repeated failed auth attempts.

    Learn more

    Session fixation and session hijacking are two related attacks. Session hijacking (this challenge) involves stealing a valid session token from another user. Session fixation is when an attacker forces a user to use a known session ID before authentication.

    Sessions that never expire (or have extremely long timeouts) are a specific vulnerability class. The admin's session in this challenge was created when they logged in and configured the system, then left running indefinitely. This is realistic: administrators often have persistent sessions on internal tools, and if those tools are misconfigured or compromised, all active sessions become attack vectors.

    Real-world session hygiene best practices include: absolute session timeouts (force re-login after N hours regardless of activity), idle timeouts (invalidate after N minutes of inactivity), single-session enforcement (new login invalidates old sessions), and session listing in user account pages so users can see and revoke active sessions.

  3. Step 3
    Hijack the admin session
    Observation
    I noticed the application authenticates users purely via a cookie named 'session' set at login, which suggested that replaying the admin's token in a -b cookie flag would make the server treat my request as the admin's without any password.
    Use the admin's session token as a cookie to authenticate as admin without knowing the password. The cleanest way is curl with -b; in a browser, paste the token via DevTools and refresh the page.
    bash
    curl -b 'session=mL_C8lz...vrIsg' http://<HOST>:<PORT_FROM_INSTANCE>/admin
    bash
    curl -b 'session=mL_C8lz...vrIsg' http://<HOST>:<PORT_FROM_INSTANCE>/flag

    Browser equivalent: open DevTools, switch to the Application tab, find the cookie under Storage, paste the admin token over the existing value, then reload /admin. The browser sends the new cookie and the server treats every subsequent request as the admin's.

    What didn't work first

    Tried: Sending the token as a Bearer Authorization header instead of a cookie

    This application uses cookie-based sessions, not Bearer token authentication. Sending 'Authorization: Bearer mL_C8lz...' is silently ignored by the Flask session middleware, which only reads the 'session' cookie. The server returns a 401 or redirects to /login as if no authentication was provided. Always match the token delivery mechanism to how the application issues it.

    Tried: URL-encoding or Base64-encoding the session token before placing it in the -b flag

    The raw session ID from the /sessions dump is already the literal cookie value the server expects. Encoding it transforms the string into a different value that does not match any Redis key, so the lookup fails. Pass the token exactly as it appears in the dump, with no additional encoding.

    Learn more

    HTTP is a stateless protocol; each request is independent. Cookies are how browsers maintain stateful sessions: the server sets a cookie on login, and the browser automatically includes it with every subsequent request to that domain. Replaying a stolen cookie via curl -b is exactly what a browser does, so the server cannot distinguish the attacker from the legitimate user.

    This is why HttpOnly and Secure cookie flags matter. HttpOnly prevents JavaScript from reading the cookie (mitigating XSS-based session theft), while Secure ensures the cookie is only sent over HTTPS (preventing interception on unencrypted networks). Neither flag helps here since we're using a server-side exposure, but they protect against the more common theft vectors.

    In a browser, you can manually set cookies using the developer tools console: document.cookie = "session=TOKEN". For API testing with curl, the -b flag sends cookie values, and -c cookie.jar saves received cookies to a file for reuse across requests, useful when exploiting multi-step vulnerabilities.

  4. Step 4
    Read the flag
    Observation
    I noticed the /admin route returned content specific to the admin role once the hijacked session cookie was attached, which suggested the flag would be rendered directly in the admin page's HTML response.
    After replacing the session cookie with the admin's token, return to the main page. The application recognises you as admin and displays the flag there.
    bash
    curl -b 'session=mL_C8lz...vrIsg' http://<HOST>:<PORT_FROM_INSTANCE>/admin | grep -oE 'picoCTF\{[^}]+\}'
    Learn more

    If the flag isn't in the obvious admin landing page, look at the diff between admin and non-admin views: navigation entries, dropdowns, hidden form fields, or API endpoints only exposed to elevated users. curl -s ... | diff - against your own session's response is a quick way to spot the privileged surface.

    This is also a good moment to check what an admin can do, not just see. Many CTF challenges scaffolded around stolen sessions hide the flag behind a state change (creating a post, approving a record, exporting data), so try POST endpoints with the hijacked cookie too.

Interactive tools
  • Flask Session DecoderDecode Flask / itsdangerous session cookies. Splits payload, decompresses zlib, parses JSON, and verifies the HMAC signature when given the secret.

Flag

Reveal flag

picoCTF{s3ss10n_t1m30ut_...}

The admin's session token is visible at /sessions and never expires due to a misconfigured session timeout.

Key takeaway

Session tokens are bearer credentials: whoever holds the token is treated as the authenticated user, regardless of how they obtained it. Exposing session listings through debug or admin endpoints turns every active token into an open door, and tokens that never expire compound the risk indefinitely. The same session hijacking primitive appears in XSS cookie theft, network interception on unencrypted connections, and server-side session store misconfigurations across web frameworks.

How to prevent this

Two compounding bugs here: a debug endpoint shipped to prod, and sessions that never die. Fix both.

  • Set absolute and idle session timeouts. 24h absolute, 30min idle is a reasonable default for most apps; admin sessions should be shorter. Rotate the session ID on every privilege escalation.
  • Never expose a list of active sessions outside the user's own account context. /sessions should return your sessions only, and the token values must be redacted (show last 4 chars at most).
  • Audit routes before each release. /admin, /debug, /sessions, /.env, /.git should be either auth-gated or 404. Tools like nuclei, ffuf, and gobuster in CI catch these in seconds.
  • Bind sessions to a fingerprint (User-Agent hash plus a coarse IP class). Any drift forces re-auth. This won't stop a determined attacker on the same network, but it raises the cost of replaying a stolen cookie from a different country, which is the common case.

Related reading

Want more picoCTF 2026 writeups?

Tools used in this challenge

What to try next