itsourcecode Baptism Information Management System 1.0 的 pbaptism.php 负责保存编辑结果。它虽然通过 POST 接收大量业务字段,却从 URL 查询字符串取出 b_id,再把这个值交给数据库模型拼接 UPDATE 的 WHERE 条件。处理表单字段的转义逻辑没有覆盖这个独立的定位参数。
CVE-2026-108927 对应 VDB-416750,报告者为 YuLi。公开报告使用 SLEEP(5) 检查 SQL 表达式执行;源码进一步说明,为什么已经转义的 SET 字段并没有保护 WHERE,以及为何验证这个入口必须同时关注耗时和数据写入。
漏洞记录与材料范围
- 影响版本
- itsourcecode Baptism Information Management System 1.0
- CWE
- CWE-89
- disclosed
| 项目 | 已确认信息 |
|---|---|
| 入口 | POST /pbaptism.php?b_id=... |
| 触发分支 | 请求体存在 submit 字段 |
| 注入参数 | URL 中的 b_id,不是 POST 字段 bapt_id |
| SQL 汇点 | Baptism::update() 的 WHERE 条件 |
| 修复版本 | 所核对的公告未列出 |
CNA 为 VulDB,CVSS 4.0 记录为 5.3/中危,与漏洞墙一致;官方同时列有 CVSS 3.1 基础分 6.3。本文保留官方 PR:L,不因请求摘录缺少 Cookie 就改写权限要求。
源码来自产品页面的 baptism.zip,SHA-256 为 22294a9f4470b22da0c31f0dc59f655bdb08452899d13c3e50705a2cdb48c3ab。公开截图可见 X-Powered-By: PHP/5.6.40,这只是该报告响应的环境线索,并未给出可确定的数据库版本。以下代码分析基于下载快照,耗时描述来自公开材料,不是新的完整环境复现。
两种输入位置进入同一个更新动作
if (isset($_POST['submit'])) {
$b_id = $_GET['b_id'];
// ... populate the Baptism object from form fields ...
$baptism->update($b_id);
// ... update personal and sponsor details ...
}这是保留关键控制流的摘录,省略部分只代表原文件中的赋值及后续更新。源码在第 5 行通过 isset($_POST['submit']) 判断进入处理分支,第 7 行读取 $_GET['b_id'],第 59 行传入 $baptism->update($b_id)。同一请求既有 URL 查询参数,也有表单请求体,两者需要分别跟踪。
bapt_id 是业务字段,b_id 是 UPDATE 的定位条件,拼写相似却走不同路径。只检查表单正文、只给业务编号加校验,或只在浏览器隐藏字段上加限制,都可能遗漏真正的注入点。
请求体还携带个人详情与赞助人资料。文件随后分别调用 secdetails->update() 与 sponsor->update();所以测试这个入口属于写路径测试,主 UPDATE 没命中也不等于整个请求没有副作用。
被遗漏的 WHERE 参数
public function update($id=0) {
global $mydb;
$attributes = $this->sanitized_attributes();
$attribute_pairs = array();
foreach ($attributes as $key => $value) {
$attribute_pairs[] = "{$key}='{$value}'";
}
$sql = "UPDATE ".self::$tbl_name." SET ";
$sql .= join(", ", $attribute_pairs);
$sql .= " WHERE b_id=". $id;
$mydb->setQuery($sql);
if (!$mydb->executeQuery()) return false;
}sanitized_attributes() 会遍历对象属性,并对其值调用 escape_value()。第 98 行追加的 $id 是方法参数,不在那组属性里,也没有占位符或整数校验。结果是 SET 值处理和 WHERE 条件处理形成两条不同的信任路径。
底层 Database::executeQuery() 使用 mysql_query() 执行整条字符串。调用一个名字包含 sanitized 的方法,只能说明它处理了被传进去的值,这类保证并不覆盖稍后拼上的条件或标识符。
UPDATE baptism
SET /* existing assignments */
WHERE b_id = (SELECT SLEEP(5));上面的 SQL 只展示结构,不是完整查询日志。攻击输入位于无引号表达式位置,子查询可参与比较条件求值;这里不需要通过单引号退出字符串。
公开时间型请求如何解读
以下为原报告请求,主机已替换为 target.example,保留表单值以展示分支所需的上下文。只应在可重置数据副本中做这类验证,因为它携带真实的更新字段。
POST /pbaptism.php?b_id=(SELECT%20SLEEP(5)) HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 370
submit=1&bapt_id=BAP001&bkNum=1&Page=1&line=1&lName=Test&fName=Test&mName=Test&sName=Test&cur_date=2024-01-01&minister=Test&stipend=100&remarks=Test&pers_id=999999&sponsor_id=999999&birth_date=2024-01-01&age=1&birth_place=Test&father=Test&f_Origin=Test&Mother=Test&m_Origin=Test&legitimacy=Test&sponsName1=Test&s_origin1=Test&sponsName2=Test&s_origin2=Test&s_others=Test请求体经离线计算是 370 个 ASCII 字节,与 Content-Length: 370 一致。submit=1 使处理分支成立,真正变化的输入是 URL 中的 b_id=(SELECT SLEEP(5));其余表单字段不应在时间对照中一起变化。
公开截图显示 HTTP 200,工具耗时栏约为 27,007 ms,而不是“恰好延迟 5 秒”。SLEEP(5) 的参数描述单次函数调用意图;UPDATE 扫描、表达式求值次数、后续业务、网络与数据库等待都可能影响请求总时间。截图缺少正常请求分布、重复次数和服务端查询轨迹,据此反推执行次数缺乏依据。
正常完成的 MySQL SLEEP() 返回 0,但即使主更新没有匹配记录,后面的个人详情、赞助人更新仍可能执行。页面“更新成功”的 JavaScript 也不是 SQL 成功或实际变更行数的可靠证明,因为这段控制流没有用主更新的返回结果保护整段成功提示。
复核设计与影响判断
先在一次性数据库里准备可识别的测试记录,并保存三类相关表的快照。用同一会话、同一请求体建立正常请求基线;再进行少量、串行的表达式对照。记录总耗时、响应状态、数据库侧语句以及请求前后的行差异,避免把网络抖动误认为时间通道。
| 检查点 | 需要回答的问题 |
|---|---|
| 分支到达 | submit 是否存在,是否进入更新代码 |
| 输入位置 | 只变更 URL 的 b_id,而非正文 bapt_id |
| 时间证据 | 有无正常对照、重复样本和查询轨迹 |
| 数据副作用 | 三类业务记录是否发生变化 |
| 修复结果 | 非整数在查询前被拒绝,合法保存仍正常 |
| 权限边界 | 会话与具体记录授权是否在写入前完成 |
源码确认了表达式拼接入口,截图提供耗时现象,两者支持 SQL 注入结论。它们并未给出数据库内容导出、任意命令执行或完整系统接管的证据。UPDATE 上下文和数据库账号权限会影响进一步的读取、修改与可用性后果,本文按已展示材料限定结论。
原报告同时出现“登录后”和“无需认证”的矛盾说明。没有独立会话复核前,保留官方低权限前提;不要用一段没有 Cookie 的请求摘录替代认证结论。
修复模型接口与多表写入
最小修复点在模型方法:它应接收经过验证的整数 ID,并在 WHERE 中绑定值。所有可编辑字段同样使用参数,字段名则由程序里的固定白名单决定,而不是由请求提供。以下只演示一个字段的更新模式,不是完整业务补丁或厂商发布版本。
<?php
// Illustrative excerpt: update only remarks, not the full form.
$raw = $_GET['b_id'] ?? null;
$id = is_string($raw)
? filter_var($raw, FILTER_VALIDATE_INT, [
'options' => ['min_range' => 1, 'max_range' => 2147483647]
])
: false;
$remarks = $_POST['remarks'] ?? null;
if ($id === false || !is_string($remarks)) {
http_response_code(400);
exit('Invalid input');
}
// Require an authenticated, authorized session and validate CSRF first.
// Resolve related record IDs from the authorized database record.
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$stmt = $pdo->prepare(
'UPDATE baptism SET remarks = ? WHERE b_id = ?'
);
$stmt->bindValue(1, $remarks, PDO::PARAM_STR);
$stmt->bindValue(2, $id, PDO::PARAM_INT);
$stmt->execute();关联的 pers_id 和 sponsor_id 应从已授权的洗礼记录读取,再做关系校验,不把浏览器隐藏字段当成权威。完整修复还包括 CSRF 校验、失败即停止的错误处理、字段范围检查与正确的 HTML 输出编码。
下载快照中的 baptism 表使用 MyISAM。直接在代码外面包一层事务,并不会让 MyISAM 更新获得回滚能力;若要保证三类记录共同成功或失败,应先核对所有参与表并迁移到支持事务的引擎,再设计同一连接上的事务和错误路径。迁移前可暂时限制该保存入口,并在备份上验证数据修复流程。
最重要的回归条件是:即便请求绕过编辑页面直接发往保存接口,ID 仍保持为数据;任何一处更新失败都不会被“成功”提示掩盖。
披露时间线
| 日期 | 事件 | 说明 |
|---|---|---|
| 2026-09-24 | GitHub 报告 #2 创建 | 平台元数据,不当作厂商收件证明 |
| 2026-10-11 | CNA 标记 Advisory disclosed | 对应漏洞墙日期 |
| 2026-10-12 | CVE 记录发布 | 官方 datePublished |
提交编号为 997398,获批记录为 VDB-416750。现有材料未确认首次修复日期、修复版本和厂商回函。截图内部的服务器时间也不作为发现日期;时间线只保留有明确来源的事件。