How a single predictable parameter unraveled into IDOR, SQL injection, multi-school data exposure, and admin credential leakage — all from a school registration portal.
01 — the discovery
It started with a receipt URL
Nothing about the registration portal looked suspicious at first. Forms, payment confirmations, student records — routine stuff. Then I caught a parameter sitting naked in the URL.
/print_payment.php?regno=1765
No session token. No authorization header. Just a bare integer. I changed it by one.
# Before
regno=1765
# After
regno=1766→ Different student. Full registration data. No error.
Classic IDOR — Insecure Direct Object Reference. The server returns another user's record with zero authorization check. The IDs were sequential, meaning the entire database was enumerable by just counting up.
02 — the pivot
IDOR was just the entry point
While probing further endpoints I noticed the backend wasn't sanitizing inputs. The same parameter that leaked student records also fed unsanitized values into database queries.
I ran sqlmap against it.
[INFO] starting dictionary-based cracking (md5_generic_passwd)
[INFO] starting 6 processes
Database: [redacted]
Table: user
[29 entries]
userloginid | pwd | role | userpassword (MD5)
adminmg · [REDACTED] · admin panel · [hash]
management · [REDACTED] · management · [hash]
omanagement · [REDACTED] · office management · [hash]
… 26 more entries (admin, it panel, user roles across all schools)
[INFO] table dumped to CSVThe user table contained 29 entries: admins, management, IT panel users, and standard users — all with MD5-hashed passwords. Several were cracked instantly. Some accounts had plaintext passwords stored alongside the hash.
03 — the scope
It wasn't one school. It was several.
The backend was a shared multi-tenant system. Multiple school branches operated under the same codebase, the same database structure, and the same vulnerable endpoints.
Single URL→IDOR on regno→SQLi via same param→Multi-school DB dump
Changing the directory in the URL path also surfaced records from a previous academic year — historical student data, still fully accessible, same issue.
/REG_2627/ → current year records
/REG_2526/ → previous year - still exposed, same vuln04 — the irony
They knew what validation looked like
One endpoint actually had proper access control:
/SchoolPanel/validate.php → requires vcode parameter + server-side check → blocks unauthenticated access correctly
Security existed in the codebase. It just wasn't applied consistently. The dangerous endpoints were left open while one admin panel was locked down — creating a false sense of protection.
05 — impact
What this actually exposed
Student PII
Names, parent details, registration info — entire year cohorts
Admin credentials
29 users — admins, management, IT — with cracked passwords
Historical data
Previous academic year records still accessible via path swap
Multi-school scope
Shared backend meant all branches were affected simultaneously
No complex exploit required. Sequential IDs + missing auth checks = full database enumerable by anyone who could count. Minors' data included. Silent — no alerts, likely no logs.
06 — root cause
Not a clever hack. Missing basics.
The vulnerability chain wasn't sophisticated. It collapsed from three missing fundamentals applied consistently:
# What was missing 1. Authorization check on every data endpoint — not just the admin panel 2. Non-sequential or tokenized record identifiers 3. Parameterized queries / input sanitization
Security doesn't fail loudly. It fails silently — in the endpoints nobody reviewed after launch.
07 — disclosure
Reported. No data stored. No data shared.
Everything was disclosed responsibly to the affected party. No student or staff data was retained, copied, or published. Testing stopped once the vulnerability was confirmed — the IDOR alone was enough to establish scope without needing to go further.
~ OffsecKalki
#bugbounty #idor #sqli