
CVE-2026-18110을 악용하는 Python PoC 및 Nuclei 템플릿으로, Concrete CMS 9.0.0-9.5.2의 사용자 자동 완성 엔드포인트를 통한 인증되지 않은 사용자 열거 취약점입니다.
Concrete CMS 9.0.0부터 9.5.2까지는 "Preview as User" 패널과 기타 사용자 선택기 컴포넌트를 지원하는 사용자 선택기 자동완성 엔드포인트(/ccm/system/user/autocomplete)에 대한 권한 검사를 수행하지 않습니다. 이로 인해 인증되지 않은 원격 공격자가 유효한 세션 또는 자격 증명 없이 전체 내부 사용자 디렉터리(사용자 ID, 사용자 이름, 이메일 주소 포함)를 열거할 수 있습니다.
이 취약점은 CSRF 토큰이 권한 부여 제어로 (오)사용되는 전형적인 사례입니다. 토큰은 요청 무결성을 증명하지만, 호출자가 사용자 목록을 조회할 권한이 있는지에 대한 실제 보안 질문에는 결코 답하지 않습니다.
| 제품 | 영향 받는 버전 | 수정된 버전 |
|---|---|---|
| Concrete CMS | 9.0.0 – 9.5.2 | 9.5.3 |
Preview As User 패널은 concrete/routes/panels.php에 기본 경로 /ccm/system/panels 아래에 등록되어 있습니다:
GET /ccm/system/panels/page/preview_as_user
해당 컨트롤러(concrete/controllers/panel/page/preview_as_user.php)는 UserSelector::quickSelect()를 호출하여 <concrete-user-select> Vue 컴포넌트를 렌더링합니다. canAccessUserSearch()를 올바르게 검사하는 형제 메서드 selectUser()와 달리, quickSelect()는 토큰을 생성하고 삽입하기 전에 권한 검사를 수행하지 않습니다:
// concrete/src/Form/Service/Widget/UserSelector.php (취약 — 9.5.2)
public function quickSelect(string $inputName, $userID = null, array $args = []): string
{
$userSelectInstance = $userSelectInstanceFactory->createInstance($labelFormat, $includeAvatar);
// 여기에 권한 검사 없음 — 토큰이 무조건 렌더링됨
$html = <<<EOL
<concrete-user-select
access-token="{$userSelectInstance->getAccessToken()}"
label-format="{$labelFormat}"
:include-avatar="{$includeAvatar}"
...
EOL;
return $html;
}
따라서 서버에서 렌더링된 HTML 응답은 이 경로에 접근할 수 있는 모든 방문자(인증되지 않은 방문자 포함)에게 유효한 access-token을 노출합니다.
access-token 값의 형식은 다음과 같습니다:
{unix_timestamp}:{md5_hash}
여기서 해시는 서버 측에서 다음과 같이 계산됩니다:
md5( timestamp : userID : action : pepper )
timestamp — 생성 시점의 UNIX 시간으로, 토큰 접두사로 평문 전송됩니다.userID — 생성 시점의 요청자 사용자 ID이며, 인증되지 않은 방문자의 경우 0 입니다.action — 문자열 user_select:format:{labelFormat}:avatar:{includeAvatar}이며, 두 값 모두 쿼리 매개변수를 통해 공격자가 제공합니다.pepper — 설치 시 한 번 생성되어 application/config/generated_overrides/concrete.php에 저장되는 64자리 무작위 비밀 값입니다.토큰은 24시간 동안 유효하며 그 기간 내에 완전히 재사용 가능합니다 — 일회용/nonce 강제가 없습니다. 또한 하한 타임스탬프 검사도 없어 신선도 검사가 단방향입니다.
1. GET /ccm/system/panels/page/preview_as_user
↓
서버가 다음을 포함하는 HTML로 응답:
<concrete-user-select
access-token="1738012345:a1b2c3d4e5f6..."
label-format="auto"
:include-avatar="true"
...>
2. POST /ccm/system/user/autocomplete
Body: accessToken=1738012345:a1b2c3d4e5f6...
&labelFormat=auto
&includeAvatar=true
&query=a
↓
서버가 전체 사용자 목록으로 응답:
[
{"id": 1, "primary_label": "admin", "secondary_label": "[email protected]"},
{"id": 6, "primary_label": "m.rossi", "secondary_label": "[email protected]"},
...
]
view() 내부의 checkAccess() 호출은 현재 요청의 uID(게스트의 경우 0)를 사용하여 해시를 다시 계산합니다 — 토큰도 uID=0으로 생성되었기 때문에 완벽하게 일치합니다. 검사가 통과되고 전체 사용자 디렉터리가 반환됩니다.
자동완성 엔드포인트의 canAccess() / checkAccess() 게이트는 토큰 무결성(위조되지 않음, 만료되지 않음)을 검증하지만 권한 부여(이 호출자가 사용자를 검색할 권한이 있는가?)는 결코 검증하지 않습니다. CSRF 토큰 유효성은 권한 검사를 대체할 수 없습니다. 동일한 컨트롤러의 getSelectedUsers()는 각 결과에 대해 Checker::canViewUser()를 올바르게 호출하지만, view()에는 이에 상응하는 검사가 없습니다.
이 저장소에는 Python 스크립트와 Nuclei 탐지 템플릿이 포함되어 있습니다. Python 스크립트는 단일 URL에만 사용할 수 있습니다. 여러 대상을 동시에 테스트하려면 대신 nuclei를 사용하십시오. 둘 다 위에서 설명한 2단계 체인을 자동화하고 Step 2 응답에서 확인된 사용자 데이터와 일치시킵니다.
python3 CVE-2026-18110.py https://target.example.com
nuclei -t CVE-2026-18110.yaml -u https://target.example.com
테스트할 권한이 있는 시스템에만 사용하도록 의도되었습니다.
Concrete CMS 9.5.3 이상으로 업그레이드하십시오. 이 수정은 selectUser()에 이미 존재하는 canAccessUserSearch() 게이트와 일관되게 토큰 발급 단계(quickSelect())에 권한 검사를 도입하고, getSelectedUsers()에서 이미 올바르게 사용되는 Checker::canViewUser() 패턴을 반영하여 자동완성 컨트롤러의 view() 내부에 동등한 권한 검사를 추가합니다.
즉시 업그레이드가 불가능한 경우, 임시 완화책으로 웹 서버 또는 WAF 수준에서 /ccm/system/panels/에 대한 인증되지 않은 접근을 차단하는 것을 고려하십시오.
이 저장소는 교육 및 방어 보안 목적으로 게시되었습니다. 모든 테스트는 명시적 승인을 받은 시스템에 대해서만 수행되었습니다. 저자는 이 문서에 포함된 정보나 도구의 오용에 대해 책임지지 않습니다.