Skip to main content

Old Sessions picoCTF 2026 Solution

Sessions never expire, so an old admin session listed on /sessions can be adopted as your own cookie.

Published: March 20, 2026Updated: September 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 1Register, log in, and check /sessions
    Observation
    The description mentions a debug endpoint that leaks session tokens. An authenticated request to /sessions should expose the admin's stale token with no password involved.
    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{s3t_s3ss10n_3xp1rat10n5_...}

    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 enumerates paths, but /sessions needs an authenticated cookie before it returns token data; unauthenticated it just redirects or comes back empty. Log in, carry the cookie, and then it dumps everything.

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

    The server issues a token for the account you authenticated as and no other. The admin's token was created in an earlier session and lives in the server-side store, visible only through the listing endpoint, never in a header from your own login.

    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 2Find the admin session
    Observation
    Each session in the response carries a key field naming its owner, so filtering for admin points straight at the right token.
    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 -b jar.txt 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 -b jar.txt 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 takes only the ID portion; the leading 'session:' is the store's key prefix, not part of the token. Include it and the server looks up a doubled key that does not exist, and treats you as unauthenticated. Strip it first.

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

    The dump already labels each token with its user, so brute force is slower and pointless. One grep finds it in a single request, and repeated failed attempts risk a lockout or an alert.

    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 3Hijack the admin session
    Observation
    The application authenticates purely on a session cookie set at login, so replaying the admin's token in that cookie makes the server treat the request as theirs.
    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 app uses cookie sessions, not bearer tokens. An Authorization header is ignored by the session middleware, which reads only the cookie, so you get a 401 or a redirect as though you sent nothing. Deliver the token the way 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 dump is already the literal cookie value the server wants. Encode it and the string no longer matches any key, so the lookup fails. Pass it through unchanged.

    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 4Read the flag
    Observation
    With that cookie attached, the admin route returns admin-only content, and the flag is rendered right in the HTML.
    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{s3t_s3ss10n_3xp1rat10n5_...}

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

Key takeaway

A session token is a bearer credential: whoever holds it is the authenticated user, however they came by it. Exposing a session listing through a debug endpoint turns every active token into an open door, and tokens that never expire keep that door open indefinitely. The same hijacking primitive shows up in XSS cookie theft, interception on unencrypted connections, and misconfigured session stores.

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

Tools used in this challenge

Where to go next