CVE-2026-5813: PHPGurukul Course Registration SQL Injection
Updated August 18, 2026 · Written by PWNMI — see About.
PHPGurukul builds a large catalog of free PHP/MySQL student projects that show up constantly as real, deployed software — course registration portals, hospital management systems, library systems — often run as-is by whoever downloaded them, with no vendor patch process behind them at all. CVE-2026-5813 is a SQL injection in one of those, the Online Course Registration System, version 3.1 (the vendor's only release — there's no fixed version to upgrade to, only "don't run this code unauthenticated on the internet").
The backlog data pointing at this CVE's PoC repos was itself noise — a numeric substring collision with an unrelated CVE (CVE-2026-58138) — so this lab skips the PoC repos entirely and was verified directly against the vendor's own downloaded source. Vendor download: phpgurukul.com/online-course-registration-free-download.
What you'll practice: recognizing when backlog/PoC metadata is unreliable and verifying a finding against primary source instead, reading a raw SQL string built by concatenation to spot the injection point before you touch a request, and distinguishing an error-based confirmation from a boolean-based one — two different ways of proving the same injection is real.
Set up the lab
New to Docker or git, or unsure what the commands below are actually doing? See Docker for Security Labs and Git for Security Work first.
Get the lab files from pwnmihq/resources:
git clone --filter=blob:none --sparse https://github.com/pwnmihq/resources
cd resources
git sparse-checkout set cve-labs/CVE-2026-5813
cd cve-labs/CVE-2026-5813
docker compose up -d --build
The web image builds by downloading the vendor's own source zip at image-build time — no manual download step, and nothing beyond the official vendor distribution is bundled here. The db container seeds itself from the bundled onlinecourse.sql on first start. Capture the container and its IP:
CONTAINER=$(docker compose ps -q web)
IP=$(docker inspect $CONTAINER --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}')
curl -s -o /dev/null -w '%{http_code}' "http://$IP/index.php"
A 200 means the app is up.
Understanding the vulnerability
check_availability.php exists to answer one question: has this student already enrolled in this course? It does that with a query built by direct string concatenation of a POST field, no parameterization, no escaping, no auth check in front of it:
$sql = "SELECT studentRegno FROM courseenrolls WHERE course='$cid' and studentRegno=' $regid'";
$cid comes straight from $_POST['cid']. There's no session check, no CSRF token, nothing gating this file — it's reachable by anyone who can send it a POST request, matching the CVE's PR:N (no privileges required) rating. Because the value lands inside a quoted string literal in the query, closing that quote early with a single ' breaks out of the intended string context, and anything after it becomes part of the SQL the database actually executes.
Exploitation
First, confirm normal behavior with a legitimate-looking value — a plain numeric course ID:
curl -s -X POST -d "cid=1" "http://$IP/check_availability.php"
This returns the app's normal "seat available" response — nothing unusual yet. Now send a single unbalanced quote:
curl -s -X POST --data-urlencode "cid=1'" "http://$IP/check_availability.php"
This throws an uncaught mysqli_sql_exception, with a MySQL syntax error pointing at the injected quote — proof the value reaches the query unescaped and unvalidated, not sanitized or filtered anywhere along the way.
An error is suggestive, but a changed response is stronger proof the injected SQL is actually being evaluated, not just breaking the query. Send a classic tautology instead:
curl -s -X POST --data-urlencode "cid=1' OR '1'='1" "http://$IP/check_availability.php"
The response branch flips — instead of "seat available," the app now reports "Already Applied for this course." That's the OR '1'='1' clause being evaluated as part of the WHERE condition: it makes the query match a row regardless of what course was actually asked about, which is exactly the behavior an attacker would chain into a full boolean-blind extraction (walking a real login table row by row, character by character, using conditions that resolve true or false instead of relying on error messages).
Cleanup
This lab's exploitation only reads data — the two requests above don't insert, update, or delete anything in onlinecourse, so there's no application state on the target to revert. On a real, unauthorized instance of this software, the same injection point could be walked further to extract the admin table's credentials directly (the query context is a simple SELECT, and nothing stops a UNION-based follow-up) — which is why finding a live instance of software like this on an engagement is worth flagging immediately, not just noting and moving on.
Tear the lab down when you're done:
docker compose down -v
Lessons worth keeping
- String concatenation into SQL is the vulnerability, not a code-style nitpick. The fix here isn't "add input validation" as an afterthought — it's using parameterized queries so user input is never in a position to be interpreted as SQL syntax in the first place, regardless of what characters it contains.
- An error message and a behavior change are two different proofs, and you often need both. The syntax error confirms the input reaches the query unescaped; the boolean-branch flip confirms the injected clause is actually being evaluated as logic, not just breaking parsing. Relying on only one can miss cases like WAFs that suppress error output but don't actually block the injection.
- Backlog and PoC metadata can be wrong, especially for CVE IDs that are numeric substrings of other CVE IDs. Verifying against the vendor's actual source — reading the real query string yourself — is what made this lab possible after the linked PoC repos turned out to be for an unrelated bug entirely.
Next step
For another unauthenticated SQL injection reproduced the same way, see CVE-2026-63030 (WordPress Core Pre-Auth Admin Takeover), which chains a similar unsanitized query into full administrator account creation. For the next phase after confirming an injection like this — turning read access into credential extraction — see the secretsdump.py cheatsheet.
Get new write-ups in your inbox
New roadmaps, tool walkthroughs, and lab write-ups. No spam. Unsubscribe anytime.