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?
Setup
Launch the challenge instance and open the web portal.
Register an account or log in with any credentials to access your profile.
Solution
Walk me through it- Step 1Observe the profile URL structureAfter 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 3000pythonpython3 -c "import hashlib; print(hashlib.md5(b'3000').hexdigest())"bash# Output: e93028bdc1aacdfb3687181f2031765dbash# Your profile URL is something like: /profile/e93028bdc1aacdfb3687181f2031765dLearn 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
hashidorhash-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.
- Step 2Enumerate nearby user IDs to find the adminThe 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 EOFLearn 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.
- Step 3Access the admin profileThe 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: a74c3bae3e13616104c1b25f9da1f11fbashcurl http://<HOST>:<PORT_FROM_INSTANCE>/profile/a74c3bae3e13616104c1b25f9da1f11fLearn 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
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.