itsourcecode Online Admission System 1.0 的管理端汇总页面把表单字段 id 插入单引号包围的 SQL 条件,再直接输出数据库错误。公开报告利用 GTID_SUBSET() 的参数解析错误,让一个由 SQL 表达式计算出来的常量标记出现在响应中。
CVE-2026-108928 对应 VDB-416751,报告者为 YuLi。问题不是 GTID 功能本身有漏洞,而是外部输入越过了 SQL 数据边界,又借错误输出形成可观察通道。修复需要同时处理查询构造、错误传播和管理端写入控制。
漏洞记录与验证范围
- 影响版本
- itsourcecode Online Admission System 1.0
- CWE
- CWE-89
- 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 进入带引号的更新条件
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 如下,换行仅用于展示表达式结构:
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 实现上。
响应证据与复现前提
公开截图显示的决定性文本为:
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 的替换模式,不是厂商补丁。示例假定分数已经按业务规则在服务端校验和计算,学年已从有效默认记录取得;这些前提应由完整处理器落实。
<?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 发布是三个不同事件;现有资料没有建立厂商首次收件、确认或修复时间,不据此补造节点。