补丁发了四十一天,NAS 还是被锁了

十月底一批群晖 NAS 被 C3RB3R 勒索软件加密,文件名多出 .L0CK3D。攻击者用的两个洞 CVSS 9.8,不用登录就能读写任意文件,补丁 9 月 18 日就发了。这轮真正暴露的不是 DSM,是补丁到设备之间的那段空窗,以及和主数据躺在同一台机器上的那份备份。
十月底这几天,一批群晖 NAS 用户打开设备时看到的是同一个画面:共享文件夹里的文件名后面多了一截 .L0CK3D,每个目录下躺着一封 read-me3.txt。照片、文档、代码仓库,还有放在同一台机器上的备份目录,一起变成了密文。
这轮攻击用的是老勒索家族 Cerber 的一个变种,圈内叫它 C3RB3R。它这几个月一直盯着 NAS 这类设备。
真正该看的不是它,是它进门的那个洞:那个洞的补丁在 9 月 18 日就发了。
门是怎么开的
360 反勒索团队处置的几起事件,入口指向同一个地方,两个 CVE:CVE-2026-13639 和 CVE-2026-13684。
群晖自己的公告把它们定在 Critical,CVSS 3.1 基础分 9.8,向量是 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H。拆开看:网络可达、攻击复杂度低、不需要任何权限、不需要用户点任何东西,机密性完整性可用性三项全高。说人话就是,只要 DSM 的对应端口能从外面够得着,不用登录就能打。
两个洞的性质不一样。CVE-2026-13639 出在登录逻辑的随机数上(CWE-331),熵不够,本来该猜不出来的东西变得可以猜。CVE-2026-13684 出在 SCGI 组件的输出编码上(CWE-116),DSM 处理 Web 请求的那一层没把输出编好码,攻击者就能顺着它往不该写的地方写东西。
官方给的缓解措施一栏写的是:无。唯一解法是升级。
修复版本是这四档:
DSM 7.4 -> 7.4-90075
DSM 7.3 -> 7.3.2-86009-4
DSM 7.2.2 -> 7.2.2-72806-9
DSM 7.2.1 -> 7.2.1-69057-12这是 9 月 18 日的事。攻击大规模发生在十月下旬。中间隔了大约四十天。
这不是 0day
事发时传得最广的说法是"最新 DSM 也中招,群晖出了 0day,全网皆靶子"。官方研判不是这样:这波利用的是已经修好的旧洞。
这个区别很重要,因为它决定了你该怎么办。如果是 0day,除了拔网线你确实没什么可做。如果是 1day,那问题就不在群晖,在你。
1day 攻击的逻辑很朴素:补丁发布不等于设备更新。漏洞公开那一刻,攻击方和防守方拿到的是同一份信息,但两边的动作速度完全不同。防守方要排窗口、要担心升级出事、要等一个"合适的时间";攻击方只需要写一段扫描脚本,然后挂在那里跑。空窗期越长,可收割的面积越大,而且这段面积是公开可查的:版本号在互联网上是能扫出来的。
我见过太多 NAS 的实际状态。装好那年是最新版,之后三年没动过。5000 端口直接映射出去,admin 密码还是初始那套,SSH 和 FTP 开着,问起来就是"以后可能用得到"。
黑群晖(非官方渠道装的 DSM)在这轮里风险更高,不是因为系统本身更脆弱,是因为这条路拿不到官方的自动更新通道,补丁到设备之间那四十天会被拉长成无限长。
还有一点要说清楚:确实有报告说跑了最新版 DSM 的机器也被加密了。这事的结论官方还没给。可能的解释是自装的套件、弱口令、或者别的暴露面,而不是"最新版有洞"。看到"最新版中招"就推出"最新版有洞",是把两件事搞混了。
备份为什么跟着一起没了
这一轮最扎心的不是数据被锁,是备份被锁。
很多人买 NAS 的初衷就是做备份。装好之后,主数据在共享文件夹,备份放在同一个共享文件夹的另一个目录,或者挂在同一台机器上的 Hyper Backup 目录。平时看着是两份,勒索软件眼里是一份:它拿到的是系统底层的执行权限,从根目录多线程往下扫,遍历所有挂载的存储,凡是这台机器能写的,它都能加密。
RAID 在这儿帮不上忙。RAID 1、RAID 5、RAID 6 防的是物理硬盘损坏,它 redundant 的是盘,不是文件。操作系统层面的恶意改写,RAID 会忠实地把它同步到每一块盘上。你花在冗余上的钱买的是硬盘故障的保险,这张保单不覆盖勒索。
快照也一样,要分两种。DSM 里那个黄色挂锁标记,意思是"别被自动清理策略删掉",它防的是清理策略,不防拿到 root 的人,撕掉它一秒钟。真正有用的是蓝色盾牌的不可变快照,保护期内谁都删不掉,改系统时间也没用,它按硬盘实际通电小时数独立倒计时。这个功能的门槛是 DSM 7.2 以上、Btrfs 文件系统、20 系及以后的机型,DS918+ 那批老机器不在官方支持列表里。
360 在样本分析里还原了它的作业习惯:先跳过 27 个系统目录(/boot、/proc、/sys、/dev、/etc 这些),把系统加密到崩溃对它没好处;跳过常见可执行文件和脚本;跳过已经加密过的文件。剩下的才是它要锁的。每生成一个 256 位密钥加 64 位 nonce,用 ChaCha8 加密文件内容,再用内置的 RSA 公钥把密钥和加密位置这些元信息包起来,附在密文尾部。各家对算法的描述不完全一致,有报道写的是 RSA-4096 配 AES 或 ChaCha20,但结论是一致的:没有通用免费解密工具,数学上不可行。
赎金信里要求用比特币付款,单一媒体报道的区间是三千到六千美元。付不付是另一回事,付了也没有任何人能保证给你密钥。
同一周的另外两个入口
这轮攻击不只一条路。同一周公开的另外两个洞,也能通向 NAS。
PHP2Shell。 CVE-2026-109618 和 CVE-2026-109619,CVSS 4.0 下 9.2 分,未授权远程代码执行。一个是内存耗尽后的清理不干净,一个是请求之间残留的全局状态。PHP 官方 10 月 21 日发补丁,CISA 在 10 月 26 日把它们加进了 KEV 目录,意味着有实际在野利用的证据。影响所有处理 Web 请求的 PHP 安装方式,官方认为 Apache 的 mod_php 风险最高,PHP-FPM、php-cgi、FrankenPHP、LiteSpeed 都在内。NAS 上跑的那些 PHP 套件,走的正是这条线。修复版本是 PHP 8.5.12、8.4.27、8.3.36、8.2.35,装完还要重启 PHP worker 或者 Web 服务。
GitLab。 CVE-2026-97229,CVSS 10.0,攻击者通过 CI/CD 配置里构造的正则表达式执行代码,不需要登录。CISA 10 月 29 日入 KEV。Alpine Linux 公开说自己的实例在 10 月 20 和 21 日被人试过,worker 进程崩了八次。不少团队在 NAS 的 Docker 里自建 GitLab,端口直接映射出去,这条路就通了。
这两条合到一起,指向同一件事:NAS 早就不是一台只会存文件的盒子了。它上面跑着容器、跑着 PHP 应用、跑着自建的代码仓库,每一个都是一扇门。你按"存储设备"的态度管它,它按"服务器"的方式被打。
一个能跑的自查
下面这个脚本干两件事。第一件算补丁暴露窗口,你给它漏洞公布日和你的设备更新日期,它告诉你裸奔了多少天。第二件检查备份是不是真能救你,按 3-2-1 的口径算:三份副本、两种介质、一份离线。
#!/usr/bin/env python3
"""NAS 暴露窗口与备份可恢复性自查。
用法:
python3 nas_readiness.py # 用内置示例数据跑
python3 nas_readiness.py --config a.json # 用你自己的设备清单
"""
from __future__ import annotations
import argparse
import json
import os
from datetime import date, datetime
# 漏洞公布日 -> 涉及的 CVE(补丁同日发布)
CVE_TIMELINE = {
"2026-09-18": ["CVE-2026-13639", "CVE-2026-13684"], # DSM,修到 7.4-90075
"2026-10-21": ["CVE-2026-109618", "CVE-2026-109619"], # PHP2Shell,修到 8.5.12
"2026-10-29": ["CVE-2026-97229"], # GitLab CI/CD 正则 RCE
}
CONFIG_SAMPLE = {
"devices": [
{"name": "客厅 NAS", "last_update": "2026-06-02", "exposed": True},
{"name": "公司 NAS", "last_update": "2026-09-20", "exposed": False},
],
"backups": [
{"path": "/volume1/backup", "same_device": True,
"last_ok": "2026-10-30", "verified_restore": None},
{"path": "/Volumes/OfflineDisk", "same_device": False,
"last_ok": "2026-08-11", "verified_restore": "2026-05-04"},
],
"snapshot_immutable": False,
}
MAX_PATCH_GAP = 14 # 补丁发布后允许的最长更新延迟(天)
MAX_BACKUP_AGE = 7 # 备份新鲜度上限(天)
MAX_RESTORE_AGE = 90 # 恢复演练有效期(天)
def days_between(start: str, end: str) -> int:
"""两个 YYYY-MM-DD 之间隔了多少天。"""
a = datetime.strptime(start, "%Y-%m-%d").date()
b = datetime.strptime(end, "%Y-%m-%d").date()
return (b - a).days
def check_patch_window(devices: list[dict], today: str) -> list[dict]:
"""只看还没补上的洞:公布日晚于设备最后一次更新的,就是未修补。"""
rows = []
for dev in devices:
unpatched = [pub for pub in CVE_TIMELINE
if days_between(dev["last_update"], pub) > 0]
gap = max((days_between(pub, today) for pub in unpatched), default=0)
if not unpatched:
status = "已修补"
elif gap <= MAX_PATCH_GAP:
status = f"窗口内,还剩 {MAX_PATCH_GAP - gap} 天"
else:
status = "已超窗"
rows.append({"name": dev["name"], "exposed": dev["exposed"],
"unpatched": len(unpatched), "gap": gap, "status": status})
return rows
def check_backups(backups: list[dict], immutable: bool, today: str) -> dict:
"""按 3-2-1 口径清点:几份副本、有没有离线的、有没有真恢复过。"""
restored = [b for b in backups if b["verified_restore"]
and days_between(b["verified_restore"], today) <= MAX_RESTORE_AGE]
return {
"副本数": len(backups),
"可达副本": sum(1 for b in backups if os.path.exists(b["path"])),
"离线副本": sum(1 for b in backups if not b["same_device"]),
"七天内更新过": sum(1 for b in backups
if days_between(b["last_ok"], today) <= MAX_BACKUP_AGE),
"九十天内验证过恢复": len(restored),
"最旧一份距今": max((days_between(b["last_ok"], today)
for b in backups), default=-1),
"不可变快照": immutable,
}
def verdict(rows: list[dict], backup: dict) -> list[str]:
"""把检查结果翻成人要做的事。"""
todo = []
for r in rows:
if not r["unpatched"]:
continue
tip = "先断公网再升级" if r["exposed"] else "排窗口升级"
todo.append(f"{r['name']}:还有 {r['unpatched']} 个洞没补,"
f"最长已裸奔 {r['gap']} 天,{tip}")
if not backup["离线副本"]:
todo.append("没有离线副本,现有备份都能被同一台机器上的勒索软件加密")
if not backup["九十天内验证过恢复"]:
todo.append("近九十天没有一份备份被真正恢复过,备份存在不等于能恢复")
if backup["最旧一份距今"] > MAX_BACKUP_AGE:
todo.append(f"最旧一份备份已经 {backup['最旧一份距今']} 天没更新")
if not backup["不可变快照"]:
todo.append("快照没开不可变保护,拿到 root 的人能删掉它")
return todo or ["当前配置没有明显缺口"]
def render(rows: list[dict], backup: dict, todo: list[str], today: str) -> None:
print(f"自查日期 {today}\n")
print("== 补丁窗口 ==")
for r in rows:
net = "公网" if r["exposed"] else "内网"
print(f" {r['name']:<10} {net:<6} 未补 {r['unpatched']} 个 "
f"已裸奔 {r['gap']:>3} 天 {r['status']}")
print("\n== 备份可恢复性 ==")
for k, v in backup.items():
print(f" {k}: {v}")
print("\n== 该做的事 ==")
for i, t in enumerate(todo, 1):
print(f" {i}. {t}")
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--config", help="设备与备份清单的 JSON 路径")
ap.add_argument("--today", default=date.today().isoformat())
args = ap.parse_args()
if args.config:
cfg = json.load(open(args.config, encoding="utf-8"))
else:
cfg = CONFIG_SAMPLE
print("没有给 --config,下面是内置示例数据的结果\n")
rows = check_patch_window(cfg["devices"], args.today)
backup = check_backups(cfg["backups"], cfg["snapshot_immutable"], args.today)
render(rows, backup, verdict(rows, backup), args.today)
if __name__ == "__main__":
main()用示例数据跑出来的结果长这样:
== 补丁窗口 ==
客厅 NAS 公网 未补 3 个 已裸奔 45 天 已超窗
公司 NAS 内网 未补 2 个 已裸奔 12 天 窗口内,还剩 2 天
== 备份可恢复性 ==
副本数: 2
可达副本: 0
离线副本: 1
七天内更新过: 1
九十天内验证过恢复: 0
最旧一份距今: 83
不可变快照: False
== 该做的事 ==
1. 客厅 NAS:还有 3 个洞没补,最长已裸奔 45 天,先断公网再升级
2. 公司 NAS:还有 2 个洞没补,最长已裸奔 12 天,排窗口升级
3. 近九十天没有一份备份被真正恢复过,备份存在不等于能恢复
4. 最旧一份备份已经 83 天没更新
5. 快照没开不可变保护,拿到 root 的人能删掉它第一行里的"未补 3 个"说的是这台机器最后一次更新停在六月,之后公布的三批洞一个都没装上;"已裸奔 45 天"说的是其中最早那批从公布到现在过了多久。第二台机器只漏了最近两批,还在十四天的窗口里,所以它排的是升级窗口而不是应急。
最该盯的是第三条。绝大多数人的备份从来没有被真正恢复过一次,备份的存在感和它的可用性是两件事,只有恢复演练能把这两件事对上。第四条同理:一份八十三天没更新的备份,在出事那天能救回来的数据比你以为的少得多。
脚本里的阈值是我拍的:十四天补丁窗口、七天备份新鲜度、九十天演练有效期。你可以按自己的情况改,但别把它改成让自己舒服的数字。
今晚就该做的六件事
按投入产出排,从上往下做:
断公网。 登录路由器,把 NAS 管理端口的映射删掉。5000 和 5001 别裸在外面。要远程访问,走 VPN 或者厂商的中继服务。这一步是唯一能立刻把风险降到接近零的动作,其他都是降低概率。
升级 DSM 和套件。 控制面板里检查更新,然后打开自动更新。套件也要一起升,很多 NAS 上出事的不是 DSM,是套件。
清点端口。 SSH、FTP、WebDAV、Docker 里映射出去的每一个端口,问一遍"这个现在还需要吗"。不需要就关。每多开一个就多一扇门,而这些门你平时根本不会去看。
改账号。 停用默认的 admin,新建一个强密码管理员账号,开两步验证,开自动 IP 封锁。
开不可变快照。 认准蓝色盾牌那个,黄色挂锁不算。保护期官方建议七到十四天。老机型不在支持列表里的,别指望它,把预算花在离线备份上。
做一份真离线的备份。 三份副本、两种介质、一份离线。那份离线的平时不要和 NAS 连着,连着的就不叫离线。存完还要定期恢复一次看看,能恢复的才叫备份。
最后一句
这轮攻击里没有任何技术上的新东西。漏洞是旧的,补丁是现成的,加密方式是教科书式的混合加密,被锁的人里有相当一部分三年前就知道自己该升级。
NAS 这类设备的处境很尴尬:它在物理上是个家电,安静地待在角落,没人会定期去看它;它在网络上是一台服务器,跑着十几个服务,天天被全网的扫描脚本摸。这两件事之间的落差,就是攻击者收租的地方。
补丁发出来的那一刻,漏洞就公开了。你的设备什么时候装上它,是你唯一能决定的变量。