Hashgate picoCTF 2026 Solution

Published: March 20, 2026

Description

You have gotten access to an organisation's portal. Submit your email and password, and it redirects you to your profile. But be careful: just because access to the admin isn't directly exposed doesn't mean it's secure. Can you find your way into the admin's profile and capture the flag?

Launch the challenge instance and open the web portal.

Register an account or log in with any credentials to access your profile.

  1. Step 1Observe the profile URL structure
    After logging in, look at your profile URL. The 32-hex-character path segment is the shape of an MD5 hash, and a quick verify of MD5(your_user_id) confirms it: user ID 3000 maps to e93028bdc1aacdfb3687181f2031765d.
    bash
    # Verify: MD5 of 3000
    python
    python3 -c "import hashlib; print(hashlib.md5(b'3000').hexdigest())"
    bash
    # Output: e93028bdc1aacdfb3687181f2031765d
    bash
    # Your profile URL is something like: /profile/e93028bdc1aacdfb3687181f2031765d
    Learn more

    This is an example of Insecure Direct Object Reference (IDOR), OWASP's top web security risk. The server uses a predictable identifier (an MD5 hash of a sequential user ID) to gate access to profile pages, without verifying that the logged-in user is actually authorised to view that profile. Hashing the ID gives the appearance of obscurity but provides zero access control because:

    • MD5 is deterministic: the same input always produces the same hash
    • Sequential integer inputs produce a small, enumerable set of hashes
    • MD5 is not a secret: anyone can compute it

    Identification here is by shape, not by tooling: a 32-hex-character string is almost certainly MD5 (or some 128-bit digest like NTLM, but MD5 is the default guess). Hash format families are tagged by tools like hashid or hash-identifier; they would tell you "possible MD5," but they do not tell you the input. Confirm by computing MD5 of your own user ID and matching it to your URL. See the hash cracking guide for more on hash identification.

    Security through obscurity is not security. The URL structure should be considered public knowledge once any user can see it, because the algorithm for generating it is entirely predictable. Proper access control requires checking who is logged in against whose resource is being requested on every request, not just at login time. See the web bug patterns post for more IDOR pattern variants.

  2. Step 2Enumerate nearby user IDs to find the admin
    The admin account is older, so its ID is a low number. Start near ID 1 and widen outward; detect the admin profile by looking for picoCTF{ in the response body, not by knowing the ID up front.
    python
    python3 << 'EOF'
    import hashlib
    import requests
    
    BASE = "http://<HOST>:<PORT_FROM_INSTANCE>"
    
    # Walk low IDs first (admins are usually created early), then widen.
    # Detect success by picoCTF{ appearing in the response body.
    for uid in list(range(1, 100)) + list(range(2900, 3100)):
        h = hashlib.md5(str(uid).encode()).hexdigest()
        r = requests.get(f"{BASE}/profile/{h}")
        if "picoCTF{" in r.text:
            print(f"HIT at ID {uid}  /profile/{h}")
            print(r.text)
            break
    EOF
    Learn more

    IDOR enumeration is the process of systematically iterating over object identifiers to access resources belonging to other users. Because user IDs are sequential integers (assigned in order of account creation), knowing your own ID gives you a starting point for enumeration. The admin account was created early, so its ID is close to other low-numbered accounts.

    The Python hashlib.md5(str(uid).encode()).hexdigest() call replicates exactly what the server computes. By precomputing hashes for a range of IDs and making HTTP GET requests to each profile URL, you can iterate through all plausible admin IDs in seconds. This is what automated IDOR scanningtools like Burp Suite's Intruder or custom scripts do in real penetration tests.

    In real bug bounty programs, IDOR vulnerabilities consistently rank among the highest-paid findings because they directly expose user data. Even a simple IDOR on a non-sensitive endpoint (like user IDs in a public profile) can be chained with other bugs to achieve account takeover or data exfiltration at scale. Responsible disclosure of IDOR bugs has earned researchers hundreds of thousands of dollars from major platforms.

  3. Step 3Access the admin profile
    The admin is at ID 3019. Navigate to their profile URL to read the flag.
    python
    python3 -c "import hashlib; print(hashlib.md5(b'3019').hexdigest())"
    bash
    # Output: a74c3bae3e13616104c1b25f9da1f11f
    bash
    curl http://<HOST>:<PORT_FROM_INSTANCE>/profile/a74c3bae3e13616104c1b25f9da1f11f
    Learn more

    MD5 (Message Digest 5) was designed as a cryptographic hash function but is now considered cryptographically broken - it is vulnerable to collision attacks (two different inputs that produce the same hash). However, the vulnerability here is not about MD5 collisions; it is about using any deterministic, reversible-by-enumeration scheme as a substitute for proper access control.

    The correct defence is server-side authorisation on every endpoint: when a request for /profile/<hash>arrives, look up which user ID corresponds to that hash, then check whether the currently authenticated user's session is allowed to view that user's data. If not, return HTTP 403 Forbidden. No amount of hash obfuscation in the URL replaces this check.

    Additional defences include using UUID v4 (random 128-bit identifiers) instead of sequential integers for user IDs - these are not enumerable. But even with UUIDs, server-side authorisation checks remain mandatory because a UUID leak (from a URL in an email, a log file, etc.) would still allow unauthorised access without an authorisation check.

Alternate Solution

Once you have confirmed the URL is an MD5 of the user ID by shape (32 hex characters), generate MD5 hashes for sequential user IDs directly from the command line with echo -n "3019" | md5sum and curl each profile URL to find the one that renders the flag, no Python required.

Flag

picoCTF{h4sh_g4t3_byp4ss3d_...}

Profile URLs are MD5 hashes of numeric user IDs. Guest is ID 3000 (MD5 e93028b...). Enumerate IDs near 3000 - the admin is at ID 3019 (MD5 a74c3ba...) and their profile displays the flag.

How to prevent this

This is IDOR dressed up in a hash. The hash is decoration; the missing authz check is the bug.

  • On every request that reads a resource, check the session against the resource owner: if (resource.user_id !== session.user_id) return 403. Hash, UUID, or sequential ID in the URL is irrelevant if this check is missing.
  • Use unguessable random IDs (UUIDv4 or 128-bit secrets) for resources, but treat them as defense-in-depth, not as authz. UUIDs leak through logs, referers, browser history, and email links.
  • Centralize authz: a single middleware or policy layer (Casbin, Oso, OpenFGA) so every controller goes through it. Decentralized per-handler checks are how the third endpoint forgets and ships the bug.

Want more picoCTF 2026 writeups?

Useful tools for Web Exploitation

Related reading

What to try next