
PoC em Python e template Nuclei que exploram o CVE-2026-18110, uma falha de enumeração de usuários não autenticada no Concrete CMS 9.0.0-9.5.2 através do endpoint de autocomplete de usuários.
O Concrete CMS 9.0.0 até 9.5.2 não realiza uma verificação de autorização no endpoint de autocomplete do seletor de usuários (/ccm/system/user/autocomplete), que dá suporte ao painel "Preview as User" e a outros componentes de seleção de usuários. Isso permite que um atacante remoto não autenticado enumere todo o diretório interno de usuários — incluindo IDs de usuário, nomes de usuário e endereços de e-mail — sem qualquer sessão válida ou credenciais.
A vulnerabilidade é um caso clássico de um token CSRF sendo (mal)utilizado como controle de autorização: o token comprova a integridade da requisição, mas nunca responde à questão de segurança real, que é se o solicitante tem permissão para consultar a lista de usuários.
| Produto | Afetadas | Corrigida |
|---|---|---|
| Concrete CMS | 9.0.0 – 9.5.2 | 9.5.3 |
O painel Preview As User está registrado em concrete/routes/panels.php sob o caminho base /ccm/system/panels:
GET /ccm/system/panels/page/preview_as_user
Seu controller (concrete/controllers/panel/page/preview_as_user.php) chama UserSelector::quickSelect() para renderizar um componente Vue <concrete-user-select>. Diferentemente de seu método irmão selectUser(), que corretamente restringe o acesso com canAccessUserSearch(), quickSelect() não realiza nenhuma verificação de permissão antes de gerar e incorporar um token:
// concrete/src/Form/Service/Widget/UserSelector.php (vulnerável — 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;
}
A resposta HTML renderizada pelo servidor, portanto, expõe um access-token válido a qualquer visitante — incluindo os não autenticados — que consiga acessar essa rota.
O valor de access-token tem o formato:
{unix_timestamp}:{md5_hash}
Onde o hash é calculado no lado do servidor como:
md5( timestamp : userID : action : pepper )
timestamp — horário UNIX no momento da geração, enviado em texto simples como prefixo do token.userID — o ID de usuário do solicitante no momento da geração; 0 para visitantes não autenticados.action — a string user_select:format:{labelFormat}:avatar:{includeAvatar}, onde ambos os valores são fornecidos pelo atacante via parâmetros de consulta.pepper — um segredo aleatório de 64 caracteres gerado uma vez no momento da instalação e armazenado em application/config/generated_overrides/concrete.php.Os tokens são válidos por 24 horas e são totalmente reutilizáveis dentro dessa janela — não há imposição de uso único/nonce. Também não há verificação de limite inferior de timestamp, tornando a verificação de atualidade unilateral.
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]"},
...
]
A chamada checkAccess() dentro de view() recalcula o hash usando o uID da requisição atual (0 para convidados) — o que corresponde perfeitamente porque o token também foi gerado como uID=0. A verificação passa, e o diretório completo de usuários é retornado.
A barreira canAccess() / checkAccess() no endpoint de autocomplete valida a integridade do token (não forjado, não expirado), mas nunca valida a autorização (este solicitante tem permissão para pesquisar usuários?). A validade do token CSRF não substitui uma verificação de autorização. Comparado com getSelectedUsers() no mesmo controller, que corretamente chama Checker::canViewUser() por resultado, view() não possui verificação equivalente.
Um script Python + template de detecção Nuclei estão incluídos neste repositório. O script Python pode ser usado apenas contra uma única URL. Se você quiser testar vários alvos simultaneamente, use o nuclei. Ambos automatizam a cadeia de duas etapas descrita acima e correspondem a dados de usuário confirmados na resposta da Etapa 2.
python3 CVE-2026-18110.py https://target.example.com
nuclei -t CVE-2026-18110.yaml -u https://target.example.com
Destinado ao uso apenas contra sistemas que você está autorizado a testar.
Atualize para o Concrete CMS 9.5.3 ou posterior. A correção introduz uma verificação de autorização no estágio de emissão do token (quickSelect()) consistente com a barreira canAccessUserSearch() já existente em selectUser(), e adiciona uma verificação de permissão equivalente dentro de view() do controller de autocomplete, espelhando o padrão Checker::canViewUser() já corretamente utilizado por getSelectedUsers().
Se a atualização imediata não for possível, considere bloquear o acesso não autenticado a /ccm/system/panels/ no nível do servidor web ou WAF como mitigação temporária.
Este repositório é publicado para fins educacionais e de segurança defensiva. Todos os testes foram realizados contra sistemas sob autorização explícita. Os autores não são responsáveis por qualquer uso indevido das informações ou ferramentas aqui contidas.