~/posts/web/cve-2026-108927-baptism-update-sql-injection.md

CVE-2026-108927:洗礼更新接口 SQL 注入

从 URL 中的 b_id 追到 Baptism::update,解释属性转义为何遗漏 WHERE 条件。结合时间型请求、约 27 秒的截图响应和多表写路径,给出严谨的复核方法与参数化修复。

date[31:24]
read[23:16]
7 分钟
cat[15:8]
Web 安全
tags[7:0]
目录
  1. 0x00漏洞记录与材料范围
  2. 0x01两种输入位置进入同一个更新动作
  3. 0x02被遗漏的 WHERE 参数
  4. 0x03公开时间型请求如何解读
  5. 0x04复核设计与影响判断
  6. 0x05修复模型接口与多表写入
  7. 0x06披露时间线
  8. 0x07参考资料

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,以及为何验证这个入口必须同时关注耗时和数据写入。

漏洞记录与材料范围

CVE-2026-108927CVSS 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 Baptism Information Management System 1.0
CWE
CWE-89
  1. 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,这只是该报告响应的环境线索,并未给出可确定的数据库版本。以下代码分析基于下载快照,耗时描述来自公开材料,不是新的完整环境复现。

两种输入位置进入同一个更新动作

pbaptism.php:5-81 (excerpt)php
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 参数

includes/baptism.php:89-102php
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 的方法,只能说明它处理了被传进去的值,这类保证并不覆盖稍后拼上的条件或标识符。

query-shape.sqlsql
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 中绑定值。所有可编辑字段同样使用参数,字段名则由程序里的固定白名单决定,而不是由请求提供。以下只演示一个字段的更新模式,不是完整业务补丁或厂商发布版本。

parameterized-update.phpphp
<?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。现有材料未确认首次修复日期、修复版本和厂商回函。截图内部的服务器时间也不作为发现日期;时间线只保留有明确来源的事件。

参考资料

NORMAL~/posts/web/cve-2026-108927-baptism-update-sql-injection.md§--
0%zh-CN