~/posts/web/cve-2026-108928-admission-summary-sql-injection.md

CVE-2026-108928:入学汇总页 SQL 注入

沿 admin/sum.php 的表单 ID 追踪带引号的 UPDATE 条件,解析 GTID 报错标记如何形成。结合官方源码核对循环内写入、认证终止与错误传播,并给出参数化修复和回归清单。

date[31:24]
read[23:16]
7 分钟
cat[15:8]
Web 安全
tags[7:0]
目录
  1. 0x00漏洞记录与验证范围
  2. 0x01表单 ID 进入带引号的更新条件
  3. 0x02报错载荷与常量标记
  4. 0x03响应证据与复现前提
  5. 0x04业务影响与证据边界
  6. 0x05参数化与错误处理修复
  7. 0x06披露时间线
  8. 0x07参考资料

itsourcecode Online Admission System 1.0 的管理端汇总页面把表单字段 id 插入单引号包围的 SQL 条件,再直接输出数据库错误。公开报告利用 GTID_SUBSET() 的参数解析错误,让一个由 SQL 表达式计算出来的常量标记出现在响应中。

CVE-2026-108928 对应 VDB-416751,报告者为 YuLi。问题不是 GTID 功能本身有漏洞,而是外部输入越过了 SQL 数据边界,又借错误输出形成可观察通道。修复需要同时处理查询构造、错误传播和管理端写入控制。

漏洞记录与验证范围

CVE-2026-108928CVSS 5.3 MEDIUM
AVNALP
ACLH
ATNP
PRNLH
UINPA
VCHLN
VIHLN
VAHLN
SCHLN
SIHLN
SAHLN
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N/E:P
影响版本
itsourcecode Online Admission System 1.0
CWE
CWE-89
  1. disclosed
项目 已确认信息
入口 POST /admin/sum.php
参数位置 表单请求体中的 id
处理分支 isset($_POST['update'])
查询目标 summary 表,stud_id 条件
演示方法 基于数据库错误的 SQL 注入
修复版本 所核对的公告未列出

评分来自 CNA VulDB:CVSS 4.0 记录为 5.3/中危,CVSS 3.1 基础分另列为 6.3。漏洞墙和本文统一采用前者,保留 PR:L。

产品页面提供的下载包名为 Online-Admission-System-Project-In-PHP-MySQL-Source-Code.zip,SHA-256 为 62d1f2b31b71bccbdadafc481e666e19b10cc189265651d775ae93d5afa98e7b。入口位于包内 chmscIT2/admin/sum.php。以下是该快照的静态分析与公开截图解读,并未重新部署报告中的环境,也没有确认其数据库精确版本。

表单 ID 进入带引号的更新条件

admin/sum.php:117-149 (excerpt)php
if (isset($_POST['update'])) {
    $id = $_POST['id'];
    // ... calculate scores and fetch the default school year ...
    $query = "UPDATE `summary` SET `entrance_score`='$entrance',`general_ave`='$gen_av',`interview`='$interview',`total`='$total',sy_id = '".$rows4['sy_id']."' WHERE `stud_id`='$id'";
    mysqli_query($db, $query) or die(mysqli_error($db));
}

摘录省略成绩计算,保留分支条件、输入来源、UPDATE 语句与错误处理。第 118 行的 $_POST['id'] 没有经过参数绑定,第 140 行直接放进 WHERE stud_id='$id'。字段在数据库里是整数并不自动保护这个位置:查询首先被解析成 SQL,之后才发生值的比较或转换。

前端用隐藏字段保存学生编号,但浏览器隐藏不改变客户端对请求的控制权。数据层真正需要的是业务类型检查、对象授权,以及语句和值分开传递。

更值得注意的是控制流:提交处理代码位于遍历汇总结果的 while 循环内部。空结果集时它可能根本不执行;多行结果时,未终止的成功分支可能重复处理同一 POST。页面输出的 JavaScript 跳转只对浏览器生效,不会终止服务端循环。回归时需要显式覆盖空表和多行数据,而不是只对单个请求看一次状态码。

报错载荷与常量标记

以下请求保留公开报告的 114 字节表单正文,删去与漏洞无关的浏览器头,并替换目标主机。它只计算常量 1,没有查询业务表或提取真实记录。

请求
POST /admin/sum.php HTTP/1.1
Host: target.example
Content-Length: 114
Content-Type: application/x-www-form-urlencoded

id=1%27%20AND%20GTID_SUBSET(CONCAT(0x7162787a71,(SELECT%20(ELT(2361=2361,1))),0x71786b7171),2361)--%20dUaI&update=

这里的 update= 值虽然为空,字段仍然存在,因此满足 isset($_POST['update'])。URL 解码后的 id 如下,换行仅用于展示表达式结构:

decoded-id.sqlsql
1' AND GTID_SUBSET(
    CONCAT(
        0x7162787a71,
        (SELECT (ELT(2361=2361,1))),
        0x71786b7171
    ),
    2361
)-- dUaI

开头的单引号关闭原本包住 ID 的字符串,AND 引入新的表达式,末尾注释处理剩余语句。内部 2361=2361 为真,ELT(...,1) 返回第一个参数值;前后两个十六进制常量分别解码为 qbxzq 和 qxkqq,最终得到 qbxzq1qxkqq。

这个字符串不符合 GTID 集合格式。GTID_SUBSET() 需要解析其参数,因此在公开报告的数据库环境中触发带标记的错误;die(mysqli_error($db)) 又把错误写入页面。不是部署了复制功能才存在这里的 SQL 注入,也不应据此把缺陷归到 MySQL 的 GTID 实现上。

响应证据与复现前提

公开截图显示的决定性文本为:

response-excerpt.txttext
Malformed GTID set specification 'qbxzq1qxkqq'.

这个标记并非完整输入的简单回显:请求给的是十六进制常量、条件表达式和函数调用,响应给的是求值后组成的字符串。源码中的原样 SQL 拼接以及数据库错误输出,与这一观察一致。

请求只带 id 和 update,省略了正常表单中的 entrance、gen_av、interview。源码在到达 UPDATE 前会读取并计算这些值,还要查询默认评分规则和学年。不同 PHP 版本、错误处理方式和初始数据库内容可能让请求更早终止,因此这份精简请求不是所有部署上的通用环境自检。

独立复核时先准备有效的默认规则、学年和汇总行,获取一次合法保存请求并固定其字段,再仅修改 id。在可还原数据库中对比正常响应与标记错误,记录数据库语句和更新前后数据;不要把 HTTP 200 当作无异常,也不要把 HTTP 302 当作后端一定停止。

原报告同时写了登录后利用与无需认证。下载快照的 admin/include/header.php:2-5 检查会话并设置 Location,但未紧接 exit;这说明认证失败的终止语义值得单独核对,不足以替代真实部署的会话测试。本文仍按 CNA 的 PR:L 描述,不把静态发现扩写为新增的已复现认证结论。

业务影响与证据边界

此接口更新入学成绩汇总,包括入学分数、平均成绩、面试、总分及学年。SQL 结构被外部输入控制时,记录选择和数据库错误通道都可能越出业务预期;数据库账号权限决定可触及的进一步范围。

公开 PoC 证明的是 SQL 表达式求值后经错误文本泄露常量,不是已经读取学生资料或改变成绩的证明。受影响语句为 UPDATE,但示例在表达式求值中触发错误,因此实际行变更还需通过数据库状态验证。对操作系统命令执行、整库导出和其他版本的影响,现有材料均没有给出证据。

建议的检测重点是进入 sum.php 的异常 ID、GTID 参数解析错误,以及失败请求与数据库错误日志的关联。记录时使用请求关联 ID、入口和结果分类,避免把完整会话、学生资料或所有表单字段写入日志。按函数名做黑名单可以辅助告警,并未修复拼接边界。

参数化与错误处理修复

下面是现代 PDO MySQL 的替换模式,不是厂商补丁。示例假定分数已经按业务规则在服务端校验和计算,学年已从有效默认记录取得;这些前提应由完整处理器落实。

parameterized-summary-update.phpphp
<?php
// Illustrative replacement after authentication, authorization and CSRF.
$raw = $_POST['id'] ?? null;
$id = is_string($raw)
    ? filter_var($raw, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 1, 'max_range' => 2147483647]
    ])
    : false;
if ($id === false) {
    http_response_code(400);
    exit('Invalid id');
}
// Scores below are validated and calculated server-side.
// $schoolYearId comes from the validated default-year record.
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$stmt = $pdo->prepare(
    'UPDATE summary SET entrance_score = ?, general_ave = ?, '
    . 'interview = ?, total = ?, sy_id = ? WHERE stud_id = ?'
);
$stmt->bindValue(1, $entrance, PDO::PARAM_STR);
$stmt->bindValue(2, $gen_av, PDO::PARAM_STR);
$stmt->bindValue(3, $interview, PDO::PARAM_STR);
$stmt->bindValue(4, $total, PDO::PARAM_STR);
$stmt->bindValue(5, $schoolYearId, PDO::PARAM_INT);
$stmt->bindValue(6, $id, PDO::PARAM_INT);
$stmt->execute();

示例把成绩作为字符串绑定,与下载快照中相应列的 varchar(30) 定义一致;编号和学年则按整数处理。绑定所有值比只给 id 增加一个转义函数更清晰,也避免对“哪些字段已经安全”的隐含依赖。

将 POST 处理移到渲染循环之前,确保一个请求只执行一次业务动作。授权失败应立即结束服务端执行;更新前核对当前操作者是否能修改指定学生记录,并校验 CSRF。客户端提交的只读字段也应重新验证,关键成绩能从服务端记录获得时优先重新计算。

替换 die(mysqli_error()),将详细异常留在服务端,页面返回通用错误和关联标识。关闭详细报错只是切断这一条可见通道,参数拼接仍然存在时漏洞并没有消失。官方尚未列出修复版本,应把这些内容当作修复建议而非“升级到某版本即可”的结论。

回归用例 验收条件
合法成绩保存 只修改获授权的目标记录,处理一次
id 含引号、函数或注释 在数据库执行前拒绝
字段缺失、数组或越界编号 返回一致的输入错误
无会话、无记录权限、CSRF 失败 不进入更新
空汇总或多行汇总 处理逻辑不依赖渲染循环
数据库错误 响应不含 SQL、路径或连接详情

披露时间线

日期 事件 说明
2026-09-24 GitHub 报告 #3 创建 平台创建时间
2026-10-11 CNA 标记 Advisory disclosed 与漏洞墙一致
2026-10-12 CVE 记录发布 官方 datePublished

VulDB 提交编号为 997399,获批条目为 VDB-416751。GitHub 创建、CNA 披露和 CVE 发布是三个不同事件;现有资料没有建立厂商首次收件、确认或修复时间,不据此补造节点。

参考资料

NORMAL~/posts/web/cve-2026-108928-admission-summary-sql-injection.md§--
0%zh-CN