事件调查

Clop 的勒索网站,成了勒索目标

勒索组织 Clop 的网站被另一团伙改掉并遭索款,事后追出的 Grav 漏洞在旧分支里多留了几个月,仍在使用旧版的站点需要尽快更新,并检查此前有没有遭到入侵。

浅暖纸手绘:网站公告板原有的文件被一张新的红色通知覆盖,旁边放着表单和文件夹。
文章导航

Clop 是一个勒索组织,靠公开受害企业的资料施压。9 月 18 日,它自己的泄露网站先多出一个外人上传的文件,随后首页也被换掉了。BleepingComputer 的记者当时访问网站,确认了文件可下载和页面遭篡改。动手的另一团伙 ShinyHunters 接着向 Clop 索要八位数赎金,威胁公开据称拿到的资料。惯常发布勒索通知的一方,这次收到了别人的通知。

这场互相勒索最值得追下去的地方,藏在网站使用的软件里。Clop 的页面由 Grav 搭建。它是一套 PHP 内容管理系统,站长用它组织文章、页面和表单,许多内容直接保存在文件中。后续采访把入口指向表单临时文件处理,而 Grav 维护者确认:对应漏洞四月已经修过,旧的 1.7 分支却一直没有得到这项修复,直到事件曝光后的 9 月 24 日。

因此,普通站长也有理由关心这起事件。攻击者声称 Clop 用着更老的 1.7.43;即使把它更新到当时最新的 1.7.53.3,相关代码仍然保留着问题。真正需要回答的是:自己的站点运行哪条分支,那条分支究竟何时修好,以及补丁到来前文件有没有被改过。

1 先找到站点真正运行的那份代码

本次入口涉及 Grav 核心的 FormFlash:它为表单保存临时状态和上传文件。旧代码把表单编号拼进目录名;编号若含有路径含义,文件操作就可能离开预定目录。核心 1.7.43 的 构造函数接收编号,getTmpDir()负责拼接。修复后的 1.7.53.4 在接收时限制编号字符,后面的上传路径会拒绝空编号。只更新 Form 插件,核心里的旧实现仍可能留下。

截至 9 月 30 日,Grav 官网推荐的稳定版是 2.2.3。能够完成兼容性验证的站点应以它为部署目标;暂时还要留在 1.7 的站点,先安装 1.7.53.4 的回补,再安排迁移。2.0.0-beta.2 是四月的首次修复点,今天应使用当前稳定版本。1.7.53.4 解决本篇讨论的缺口,其他依赖和历史安全更新仍需逐项检查。

来不及更新时,先限制外部访问受影响表单及其上传、状态保存入口,并从服务器一侧确认限制生效。仅隐藏网页上的上传按钮,访问者仍可直接提交请求。暂停整个相关表单可能同时停掉咨询、附件收集等业务,应给使用者一个可用的替代渠道。恢复前,在隔离副本完成版本核对、正常提交和上传检查,再确认异常编号无法产生目录外写入。升级失败就继续保留限制;回退包也须包含本分支的修复,不能为了恢复页面把漏洞一同恢复。

2 一个表单编号,怎样决定文件写到哪里

网站收附件时,文件通常先进入临时目录,等表单处理完成后再转到最终位置。Grav 需要知道这份临时文件属于哪个会话、哪张表单,于是把两种编号放进路径。默认布局可以读成 tmp/forms/会话编号/表单编号。这样组织文件很方便,前提是这两个编号始终只是目录中的一个名字。

问题从浏览器带回的表单编号开始。报道中提到的 Form 7.3.0,在 form.php 第 1191–1215 行读取 __unique_form_id__,找到对应表单后调用 setUniqueId()。服务器最初生成过一个正常编号,也不能保证访问者原封不动地把它交回来。隐藏字段只影响网页怎样显示,字段值仍由提交请求的一方提供。

接下来,核心的 FormTrait::getFlash()把这个值作为 unique_id 交给 FormFlash。这里的 flash 指跨请求保留的短期表单状态。构造函数接下编号后,getTmpDir()几乎直接给出了文件系统会使用的路径:

// Grav 1.7.43 · FormFlash::getTmpDir()
return $this->folder && $this->uniqueId
    ? "{$this->folder}/{$this->uniqueId}"
    : '';

比如,正常编号 form_42 会落在会话目录里的同名子目录。目录分隔符和表示上级目录的片段却会改变路径走向。PHP 把拼好的字符串交给文件系统时,文件系统会按这些含义查找位置。错误发生在这里:原本用于区分表单的值,获得了选择存储位置的能力。

浏览器提交的表单编号进入 unique_id,旧版直接拼入目录,可能越出预定位置;补丁先检查字符并将非法编号置空。
图 1:编号参与目录拼接的过程。门和文件夹表示字符检查与存储位置;具体允许字符见下文代码。实际写入还受可达功能和服务器权限约束。

还要看谁真正使用了这个路径。Form 插件的 uploadFiles()会调用 flash 对象的上传方法。1.7.43 使用的兼容类 Grav\Common\Form\FormFlash::uploadFile()取得临时目录、创建目录,再调用 move_uploaded_file() 把文件搬进去。到这一步,路径字符串已经变成了磁盘写入位置。

这也解释了为什么检查上传文件的后缀、大小,或者给文件换一个随机名字,仍然各有各的局限。它们可以约束文件内容、数量或最后一段文件名;这里出问题的是父目录。插件在调用上传方法前确实有类型和大小检查,具体站点还受表单配置、功能可达性和 PHP 进程写权限限制。源码足以说明目录逃逸如何形成;Clop 机器上完整的请求、落地文件和后续执行过程,公开材料没有提供。

3 四月的修复,九月才到旧分支

把两个版本的代码并排看,补丁本身并不复杂。四月的 修复提交 d904efc为路径编号加上严格的字符限制,进入 2.0.0-beta.2。9 月 24 日,1.7 分支才通过 a39fe54 的回补获得对应处理,并随 1.7.53.4 发布。维护者在事件后续采访中确认,之前没有把修复带回 1.7。

“回补”就是把新分支的一项修复带到旧分支,让暂时无法升级大版本的用户也能修掉同一个错误。这次留出的几个月,正好说明了它的价值。对 1.7 用户,四月的修复公告还没有变成自己可以安装的同分支修复包。公开记录没有解释当时的内部安排,两条发布线确实出现了修复时间差。

1.7.53.4 的 sanitizeId()用下面的正则检查编号,匹配失败就把整个值置空。它没有把坏字符删掉后继续使用剩余部分,因而不会把两种不同输入悄悄修剪成同一个目录名。

// Grav 1.7.53.4 · sanitizeId() 的接收条件
preg_match('/^[A-Za-z0-9,_-]{1,64}$/', $id) ? $id : '';

空编号怎样阻止前面那次写入?沿同一条调用链回去看,getTmpDir()在编号为空时返回空路径;兼容类的 uploadFile()则会在创建目录和搬动文件之前返回失败。表单状态保存也有自己的空值检查。这些具体分支说明补丁改变了什么。其他调用者、插件和自定义表单仍需要按各自的路径验证,不能把一句补丁注释当作整站所有文件操作的保证。

旧分支还保留了一处有意的差别。2.x 的实现限制 id、session_id、unique_id 三个字段;1.7 回补只限制后两个。1.7 中的 id来自表单配置,可能是用于查找媒体表单的路径式标识。新增回归测试专门要求这个配置值保持原样。安全回补既要截住不可信请求,也要保留旧系统中合法的调用方式;逐字段追来源,才能理解为什么补丁没有机械地复制三行代码。

漏洞资料里还有一处容易串线。原始安全公告围绕 __form-flash-id 进入 session_id 的路径展开,描述任意目录和 index.yaml 写入;九月事件采访讨论的是 __unique_form_id__ 进入 unique_id 的路径。它们最终碰到同一类目录拼接问题,入口参数和调用条件各自不同。检测只认原公告中的一个参数名,就可能漏掉后来公开的另一条路径。

4 改了首页之后,究竟拿走了什么

讲到这里,可以理解网站内容为何会落到外人手里。数据到底丢了多少,仍要回到事件证据。记者直接观察过新上传的文件和改掉的首页;Grav 维护者确认了提交给他们的漏洞描述。ShinyHunters 进一步声称拿到日志、源码和 onion 服务私钥,并以所谓付款资料施压。这些失窃内容没有得到记者独立验证。

Clop 则承认网站的软件没有完全更新,后来换了新的 onion 地址,同时否认重要的财务或运营资料失窃。新地址和公开争执可以记录,服务器上原来保存过什么、谁读到了什么,还缺镜像、日志和可核验的文件证据。Clop 否认正在谈判,公开材料也没有确认赎金已经支付。本文的“勒索目标”指公开索款与威胁已经发生。

对自己的业务,这个区别会直接改变恢复范围。网站页面被改,至少需要调查内容和代码完整性;若能确认进程接触的凭据或私钥被读取,就要扩大到撤销、轮换和关联系统检查。把所有传闻一口气当成事实,会让处置失焦;只把首页改回来,又会留下谁还掌握访问能力的问题。恢复工作应由取得的证据一步步扩大。

5 普通站点该怎样查到可以收尾

先确认更新覆盖了正在处理请求的安装目录。多副本、旧容器、未切换的发布目录,都可能让后台显示的版本与实际服务请求的代码不一致。逐实例检查 Grav 核心版本及 FormFlash 文件,再记录表单插件版本、启用的文件字段和临时目录配置。这个清单决定哪些入口需要暂停,哪些业务需要在升级后重新走一遍。

验证可以直接借鉴上游已有的边界。1.7.53.4 的回归测试覆盖了路径分隔符、上级目录片段、流式路径写法、编码形式、超长值和控制字符,同时保留正常编号、逗号及长度 64 的合法情况。SOSEC 阅读了这些断言和相关源码,没有把它们写成本站已经运行的产品测试。站点验收应在隔离副本上执行相应检查,并同时完成一次合法文件上传和正常表单提交,观察磁盘实际写入位置。

只检查错误请求是否被拒绝还不够。某项前置配置可能让请求根本走不到被修复的代码。因此,要确认正常请求确实经过同一条表单路径,再检查异常编号受到限制;还要留意自定义媒体表单是否继续工作。1.7 补丁为配置 id保留兼容性的那条测试,正适合提醒管理员检查这个容易遗漏的正常用途。

过去的暴露则需要另一组证据。保存网站和反向代理日志、相关文件的时间信息与可信备份,检查表单临时目录之外的新文件,以及页面、插件、配置的异常修改。日志如果没有记录请求体,就可能看不到被提交的表单编号;没有关键词命中只能覆盖实际保留下来的记录。确认或高度怀疑站点被写入后,优先从可信程序包重建,审查待恢复的内容与配置,按进程可接触范围处理秘密,避免把可疑文件一起带回新环境。

最后再回到正常用户:咨询是否能提交,附件是否能取回,媒体管理是否仍可使用,所有副本是否都切到修复代码,原先的访问限制是否可以逐项撤掉。升级失败时留在隔离状态,使用已修复且完成兼容验证的包恢复;仅凭“版本不低于 1.7.53.4”不能判定跨大版本回退的数据和配置一定兼容。

这起事件真正值得带回工作的,是那个很容易在更新流程里漏掉的问题:修复落在哪条分支上?Clop 的身份让新闻格外醒目,代码中的错误却很普通。一串从浏览器送来的编号,被交给文件系统解释;一项已经写好的补丁,迟迟没有进入仍有人使用的发布线。把这两件事追到自己的运行代码、实际写入和恢复结果,才能知道这次更新到底修到了哪里。

研究依据

研究依据核对公开采访、Grav 与 Form 插件固定版本源码、两分支补丁和上游回归测试;未访问涉事暗网站点,未取得服务器镜像,未复现真实产品入侵。资料截止 2026-09-30 10:42 UTC。

来源Grav 源码与安全公告、BleepingComputer 现场观察与采访

证据置信度 中

6证据与来源

6.1来源与材料

  1. BleepingComputer:9 月 19 日现场观察与 9 月 21 日更新https://www.bleepingcomputer.com/news/security/shinyhunters-hacks-clop-leak-site-threatens-to-extort-ransomware-gang/
  2. BleepingComputer:9 月 25 日漏洞路径与维护者采访https://www.bleepingcomputer.com/news/security/shinyhunters-hacked-clop-leak-site-using-grav-cms-path-traversal-flaw/
  3. Grav:CVE-2026-42608 / GHSA-hmcx-ch82-3fv2https://github.com/getgrav/grav/security/advisories/GHSA-hmcx-ch82-3fv2
  4. Grav:四月修复提交 d904efchttps://github.com/getgrav/grav/commit/d904efc33e03ebb597afde8d3368b28cf0423632
  5. Grav:1.7 分支回补与回归测试 a39fe54https://github.com/getgrav/grav/commit/a39fe54c6ffc6c20189bd8099a754a8f2c74daa6
  6. Grav:1.7.53.4 发布说明https://github.com/getgrav/grav/releases/tag/1.7.53.4
  7. Grav:当前稳定版 2.2.3 发布说明https://github.com/getgrav/grav/releases/tag/2.2.3
  8. Grav:官方当前下载建议https://getgrav.org/downloads