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.
Setup
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).
curl -c jar.txt -b jar.txt -d 'username=test&password=test' http://<HOST>:<PORT_FROM_INSTANCE>/loginSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Register, log in, and check /sessionsObservationI 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 inbash# Then navigate to: http://<HOST>:<PORT_FROM_INSTANCE>/sessionsbash# Or with curl:bashcurl -c jar.txt -d 'username=test&password=test' http://<HOST>:<PORT_FROM_INSTANCE>/registerbashcurl -c jar.txt -b jar.txt -d 'username=test&password=test' http://<HOST>:<PORT_FROM_INSTANCE>/loginbashcurl -b jar.txt http://<HOST>:<PORT_FROM_INSTANCE>/sessionsExpected 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
keyfield 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 likeffuf,gobuster, andferoxbusterautomate 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.Step 2
Find the admin sessionObservationI 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:bashcurl -s http://<HOST>:<PORT_FROM_INSTANCE>/sessions | grep -oP "session:\K[^,]+(?=.*'key': 'admin')"bash# Or extract all IDs and probe which one reaches /admin:bashfor 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 doneWhat 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.
Step 3
Hijack the admin sessionObservationI 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.bashcurl -b 'session=mL_C8lz...vrIsg' http://<HOST>:<PORT_FROM_INSTANCE>/adminbashcurl -b 'session=mL_C8lz...vrIsg' http://<HOST>:<PORT_FROM_INSTANCE>/flagBrowser 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 -bis 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.
HttpOnlyprevents JavaScript from reading the cookie (mitigating XSS-based session theft), whileSecureensures 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-bflag sends cookie values, and-c cookie.jarsaves received cookies to a file for reuse across requests, useful when exploiting multi-step vulnerabilities.Step 4
Read the flagObservationI 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.bashcurl -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
How to prevent this
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.
/sessionsshould 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,/.gitshould be either auth-gated or 404. Tools likenuclei,ffuf, andgobusterin 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.