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 CSV

The 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 vuln

04 — 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