只需向表单发送一次 POST 请求,服务器就可能执行攻击者的代码
只需向表单发送一次 POST 请求,匿名访问者就可能在你的服务器上执行代码。下面这条请求揭示了漏洞的核心,而类似请求已经被用于攻击真实网站。CVE-2026-85097 是 Bricksforge 中的一处未授权任意文件上传漏洞。Bricksforge 是 Bricks 网站构建器的一款常用扩展插件,受影响版本为 3.1.8.9 及更早版本,3.1.8.10 起已修复。[1][2] 负责分配该 CVE 编号的 Wordfence 给出的评分为 9.8;监测到实际利用活动的 Patchstack 则将其评为 10.0。[1][2]
Bricksforge 于 9 月 3 日收到漏洞报告,相关细节于 10 月 7 日公开。[2] 当天 UTC 时间 21:47,Patchstack 就发现了首起利用尝试,攻击流量来自显然早已做好准备的自动化基础设施。[1] 我没有在真实运行的网站上复现此次攻击。以下分析依据的是 Patchstack 的技术文章及其公开的代码、Wordfence 安全公告、厂商版本更新日志和 WordPress 官方文档,资料查阅时间为 2026 年 10 月 12 日。[1][2][3][4] 真正值得关注的,并不是一次文件上传最终导致了代码执行,而是插件明明认真检查了文件,仍然被绕过。
POST /wp-json/bricksforge/v1/form_submit HTTP/1.1
Host: victim.example
Content-Type: application/json
{
"temporaryFileUploads": [
{
"field": "form-field-file",
"file": {
"file": "/var/www/.../uploads/bricksforge/tmp/img.gif", // real GIF, passed the MIME check
"url": "https://victim.example/wp-content/uploads/shell.php", // attacker names the destination
"type": "image/gif"
}
}
]
}服务器检查了文件内容,攻击者却决定了文件名
先看 Bricksforge 原本的处理逻辑。访问者附加文件时,插件会先把它上传到临时目录,并在后续处理前检查文件的 MIME 类型。这一步本身没有问题:声称是 GIF 的文件必须确实符合 GIF 格式,检查也确实有效。但随后,开发者做了一个看似无害的设计选择:正式提交表单时,由浏览器告诉服务器要处理哪个文件,以及该文件最终应该保存到哪里。
问题就在这里。仅仅说“没有验证 url 字段”,还不足以概括漏洞本质。插件通过一个字段检查文件的内容,却通过另一个可由攻击者控制、且未经再次验证的字段决定最终文件名和扩展名。被验证的属性,与实际决定后续操作的属性,并不是同一个。GIF 文件的字节内容无法证明即将写入磁盘的文件不会被命名为 shell.php。在使用 Apache 与 mod_php 的服务器上,决定服务器是否将其作为 PHP 执行的是文件末尾的 .php 扩展名,而不是图片文件头。
一张合法图片,最终被复制成可执行文件
整个过程分为两个阶段。首先是上传。攻击者从无需认证的接口获取 nonce,然后上传一个多格式混合文件(polyglot):它既是合法的 GIF,也包含一行 PHP 代码。由于文件确实符合图片格式,mime_content_type 会返回 image/gif,插件便将其原样保存到临时目录。[1] 到这一步,插件的处理并没有出错,磁盘上的文件确实是一张图片。
接下来提交表单,真正发挥作用的是元数据。请求中的 JSON 包含一个 file 对象,其中两个字段最关键:file 指向已通过验证的文件在服务器上的路径,url 则表示目标位置。下面的代码根据 Patchstack 公布的实现进行了精简,展示了存在漏洞的处理路径。[1] 程序从 url 字段取出目标名称,交给 basename 处理,再用得到的名称保存已通过验证的文件内容。
注意 copy() 实际完成了什么:它把临时目录中那张真正的 GIF 图片复制到攻击者指定的路径,并在 Web 根目录下生成一个以 .php 结尾的文件。图片文件头使它仍然是合法 GIF,而其中嵌入的 PHP 代码,则可能在有人首次请求该 URL 时被服务器执行。[1] MIME 检查无法阻止这一过程,因为它回答的是另一个问题:文件内容是不是图片。它从未证明文件最终会以图片的形式保存,更没有证明服务器会把它作为图片提供访问。
// Bricksforge Pro Forms (condensed from the vulnerable path)
$file_path = $file_data['file']; // server path of the file that was MIME-checked
$file_url = $file_data['url']; // client-supplied, never re-validated
$attachment['guid'] = $file_url; // destination comes from the URL
$desired_filename = basename($attachment['guid']); // ...so from the attacker
$destination_path = $upload_path . '/' . $desired_filename;
copy($file_path, $destination_path); // GIF bytes written to an attacker-named .php为什么“我们的表单没有文件上传功能”也不能保证安全
即使你认为自己的网站没有开放文件上传功能,下面这一点也值得警惕。你可能以为漏洞只影响允许用户上传附件的表单,但 Bricksforge 自己的更新日志明确指出:只要启用了 Pro Forms,即便表单中根本没有文件上传字段,网站仍然会受到影响。[3] 原因就在代码中:待处理的文件列表并不是从实际的 multipart 文件上传请求中提取的,而是直接读取 JSON 请求体中的 temporaryFileUploads 值。这个值可以由攻击者自行构造。[1] 因此,一个普通的纯文本联系表单也足以触发问题。
nonce 也不是看上去那么可靠的防线。Bricksforge 暴露了 bricksforge_regenerate_nonce 接口,任何匿名访问者都能通过它获取新的 nonce,这已经使它失去了作为访问门槛的意义。即使没有这个接口,WordPress 默认也会为所有未登录用户生成相同的 nonce,因为这些用户共享用户 ID 0。[4] nonce 并不能证明请求来自可信用户。WordPress 官方文档明确强调:绝不能依赖 nonce 完成身份认证、授权或访问控制。[4] 而这个请求处理器恰恰把它用在了这样的场景中。
// The file list is read from the JSON body, not from a real multipart upload:
$file_uploads = isset($form_data['temporaryFileUploads'])
? json_decode($form_data['temporaryFileUploads'], true) // attacker-supplied
: [];
$merged_files = array_merge($file_uploads, $request->get_file_params());
// The gate in front of it:
// GET bricksforge_regenerate_nonce -> hands any anonymous visitor a nonce
// WordPress gives every logged-out user the SAME nonce (user ID 0)
// A nonce is CSRF hygiene. It was never authentication.修复的关键:文件名必须由服务器决定
修复重新确立了一条基本原则:上传文件保存在哪里、叫什么名字,应由服务器决定,而不是由客户端指定。Patchstack 和 Wordfence 的公告均确认,3.1.8.10 是已修复的版本。[1][2] Bricksforge 的更新日志还指出,10 月 8 日发布的 4.0.1 版本包含文件上传问题的修复,并表示会在漏洞公开后提供完整细节。[3] 应根据所使用的版本分支和许可证升级至对应的修复版本;对于 3.x 分支,最低应为 3.1.8.10。
这条原则并不局限于 Bricksforge。已保存文件的名称本身就是一项安全决策,因此必须掌握在服务器手中:生成随机文件名,通过允许列表限定扩展名,并将文件存放在 Web 根目录之外,或放在明确禁止执行 PHP 的目录中。下面的代码只用于说明修复思路,并非厂商补丁的原始差异。此外,Bricksforge 还有一个特殊风险:它是商业插件,不在 WordPress.org 插件目录中,更新依赖有效许可证。[3] 如果许可证已过期,插件可能悄无声息地停留在存在漏洞的版本,即使后台看起来一切正常。“我们以前买过”不等于“我们已经打上补丁”。
// The fix, as a principle: the SERVER owns the name and extension, never the client.
$ext = 'bin'; // or an allowlisted image extension
$stored = wp_generate_uuid4() . '.' . $ext; // random name the attacker can't choose
$destination = $upload_path . '/' . $stored;
copy($validated_tmp_file, $destination);
// And decide execution by WRITE location, not by MIME: store outside the web root,
// or in a directory where the server is configured never to run PHP.如果漏洞出现在其他项目中,我会怎样排查
这不是 Bricksforge 独有的问题,而是一类比 WordPress 更早就存在的缺陷:验证一种属性,却依据另一种属性执行操作。只要上传处理程序检查了文件内容,却仍允许请求参数决定文件名、扩展名或保存目录,无论后端使用 PHP、Node 还是 Java,都可能出现同类漏洞。审查文件上传逻辑时,我不会先盯着大家最引以为傲的 MIME 检查,而是先找真正将文件写入磁盘的位置。
我会找出所有把字节写入磁盘的代码位置(写入点或 sink),再逆向追踪目标文件名的来源。下面的 grep 命令可以帮助筛选候选位置,但真正重要的是逐一阅读相关逻辑。对于每次写入,我都会问一个问题,而这也正是代码审查时本可以发现 CVE-2026-85097 的关键:经过验证的属性,是否真的决定了最终可被执行的属性?如果只检查了文件内容,文件名却来自另一个由客户端控制的字段,那么这道检查在关键风险面前就只是摆设。
# Smell to hunt in any upload handler:
# the stored filename, extension or directory derived from request input.
rg -n "basename\(|get_param|\$_POST|\$_REQUEST|json_decode" --glob '*.php'
# Then read each write sink by hand: move_uploaded_file, copy, file_put_contents, rename.
# The one question that confirms the bug:
# does the property you VALIDATED (the bytes, the MIME) decide the property
# that gets EXECUTED (the extension, the directory)?
# If a separate, client-controlled field names the sink, the check is decoration.这是否与你有关?只有运行 Bricksforge 的网站需要直接应对
先客观看待影响范围。根据 Patchstack 的统计,Bricksforge 约有 6,000 个活跃安装实例,因此这并不是一场波及整个互联网的大规模事件。[1] 如果网站没有使用 Bricksforge,你可以关闭这个页面。但如果正在使用,风险就绝非纸上谈兵。Patchstack 记录到,漏洞公开后数小时内,来自 63 个不同源 IP 地址的利用活动已经开始;95% 的恶意请求将 PHP 载荷伪装成 GIF 或 PNG,还有少量攻击集群反复尝试数十种扩展名写法,以绕过简单的过滤规则。[1]
这些绕过手法值得关注,因为它们直接提示了日志排查的重点。攻击者尝试过 .php7、.phtml 等其他 PHP 扩展名,使用 .PHp 这样的大小写变体,以及 %2ephp 这样的 URL 编码形式;这些尝试都针对同一处 copy() 操作。[1] 他们试图写入的文件常带有 login_admin_*.php 之类的名称,位置包括常规上传目录和 Bricksforge 的临时目录。[1] 截至 10 月 12 日,该 CVE 尚未进入美国网络安全和基础设施安全局(CISA)的已知遭利用漏洞目录(KEV),EPSS 评分也低于 1%,但这并不代表攻击活动平静。[2] 只能说明当时仍处于早期阶段。KEV 属于相对滞后的指标;蜜罐往往能更早反映攻击动向,而当时蜜罐已经监测到大量动静。
判断你的网站是否面临紧急风险,需要同时满足三个条件:使用 Bricksforge、版本低于 3.1.8.10、且站点可以从互联网访问。如果三个条件都成立,安装补丁只是第一步,而不是终点,因为攻击者此前可能已经写入并执行了 PHP 文件。网站管理员要问的不只是“我们更新了吗”,还应追问“在更新之前,是否有人携带文件载荷访问过 form_submit 接口”。答案不在插件管理界面里,而在访问日志和上传目录中。
关闭页面之前,应该先做什么
我的处理顺序会是这样:首先安装补丁,因为漏洞公开后的利用窗口按小时计算,而不是按周。[1] 但如果存在漏洞的版本曾暴露在互联网上,就应假设有人可能尝试过攻击,在完成排查前不要轻易宣布问题已经解决。补丁可以阻止下一次恶意上传,却不会自动清除上周就已经植入的 Web Shell。
下面的检查清单给出了具体操作。日志中最值得寻找的是攻击难以绕开的特征:针对表单提交接口的请求包含 temporaryFileUploads 数据,其中 file 字段指向图片,而 url 字段却以 PHP 扩展名结尾。这种不一致正是漏洞利用的重要特征,正常业务中很难无意间出现。
Patch and verify (in this order):
[ ] Find every site running Bricksforge; confirm the version actually LOADED.
[ ] Update to 3.1.8.10+ (3.x branch) or 4.0.1+. Bricksforge is license-gated and
not on WordPress.org, so a lapsed license can silently block the update.
[ ] Cannot update now? Block at the WAF / server: POST to
/wp-json/bricksforge/v1/form_submit and the admin-ajax action
bricksforge_form_submit carrying a temporaryFileUploads body.
If a vulnerable version faced the internet, assume compromise and hunt:
[ ] Grep uploads for unexpected .php, especially
/wp-content/uploads/bricksforge/tmp/ and names like login_admin_*.php.
[ ] Audit for hidden admin accounts by querying the database directly,
and review the must-use (mu-plugins) directory.
[ ] In access logs, look for form_submit requests whose file path is an image
but whose url field ends in a PHP extension.
[ ] Rotate secrets reachable from PHP: database, API keys, SMTP.“我们验证了上传文件”并不能证明上传功能安全
CVE-2026-85097 说明了一个简单却常被忽视的道理:“我们已经验证上传文件”并不能直接回答“文件上传是否安全”。插件检查了容易检查的内容,却信任了真正决定风险的字段。验证对象与执行依据之间的这道裂缝,正是许多文件上传漏洞的根源。无论是软件版本号,还是扫描器显示的绿色通过标记,都无法单独揭示它。只有沿着请求到文件写入的完整路径分析,才能看清问题。
从匿名请求出发,穿过框架的请求处理流程,一直追踪到决定磁盘文件名的那一行代码,这是 Web 应用安全评估的核心工作之一。“验证一项属性,却依据另一项属性采取行动”的危险模式,也广泛存在于与 WordPress 无关的项目中。如果你的应用包含表单、文件上传,或任何将外部输入转化为服务器文件的处理流程,这就是我会重点检查的信任边界。告诉我你正在开发什么,我们可以据此确定评估范围。
参考来源
需要在您自己的产品中检查这个问题吗?
Web 应用与 API 渗透测试想学会自己发现这类漏洞吗?
Web 应用渗透测试课程(一对一)