
Python-PoC und Nuclei-Template zur Ausnutzung von CVE-2026-18110, einer nicht authentifizierten Benutzer-Enumeration-Schwachstelle in Concrete CMS 9.0.0-9.5.2 über den Benutzer-Autocomplete-Endpunkt.
Concrete CMS 9.0.0 bis 9.5.2 führt keine Autorisierungsprüfung am Benutzerauswahl-Autocomplete-Endpunkt (/ccm/system/user/autocomplete) durch, der das Panel „Preview as User" und andere Benutzerauswahl-Komponenten unterstützt. Dies ermöglicht einem unauthentifizierten Remote-Angreifer, das vollständige interne Benutzerverzeichnis aufzuzählen — einschließlich Benutzer-IDs, Benutzernamen und E-Mail-Adressen — ohne gültige Sitzung oder Anmeldedaten.
Die Schwachstelle ist ein klassischer Fall, in dem ein CSRF-Token (missbräuchlich) als Autorisierungskontrolle verwendet wird: Das Token belegt die Integrität der Anfrage, beantwortet aber nie die eigentliche Sicherheitsfrage, ob der Aufrufer berechtigt ist, die Benutzerliste abzufragen.
| Produkt | Betroffen | Behoben |
|---|---|---|
| Concrete CMS | 9.0.0 – 9.5.2 | 9.5.3 |
Das Panel Preview As User ist in concrete/routes/panels.php unter dem Basispfad /ccm/system/panels registriert:
GET /ccm/system/panels/page/preview_as_user
Sein Controller (concrete/controllers/panel/page/preview_as_user.php) ruft UserSelector::quickSelect() auf, um eine <concrete-user-select>-Vue-Komponente zu rendern. Anders als seine Schwestermethode selectUser(), die korrekt über canAccessUserSearch() absichert, führt quickSelect() keine Berechtigungsprüfung durch, bevor ein Token erzeugt und eingebettet wird:
// 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;
}
Die serverseitig gerenderte HTML-Antwort legt daher ein gültiges access-token gegenüber jedem Besucher offen — einschließlich unauthentifizierter — der diese Route erreichen kann.
Der Wert von access-token hat das Format:
{unix_timestamp}:{md5_hash}
Wobei der Hash serverseitig wie folgt berechnet wird:
md5( timestamp : userID : action : pepper )
timestamp — UNIX-Zeit zum Zeitpunkt der Erzeugung, im Klartext als Token-Präfix gesendet.userID — die Benutzer-ID des Anforderers zum Zeitpunkt der Erzeugung; 0 für unauthentifizierte Besucher.action — die Zeichenkette user_select:format:{labelFormat}:avatar:{includeAvatar}, wobei beide Werte vom Angreifer über Query-Parameter geliefert werden.pepper — ein 64-stelliges zufälliges Geheimnis, das einmalig bei der Installation erzeugt und in application/config/generated_overrides/concrete.php gespeichert wird.Tokens sind 24 Stunden lang gültig und innerhalb dieses Zeitfensters vollständig wiederverwendbar — es gibt keine Einmal-/Nonce-Durchsetzung. Es gibt auch keine Prüfung auf einen unteren Zeitstempel-Grenzwert, wodurch die Aktualitätsprüfung einseitig ist.
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]"},
...
]
Der checkAccess()-Aufruf innerhalb von view() berechnet den Hash unter Verwendung der uID der aktuellen Anfrage neu (0 für Gäste) — was perfekt übereinstimmt, da das Token ebenfalls mit uID=0 erzeugt wurde. Die Prüfung wird bestanden, und das vollständige Benutzerverzeichnis wird zurückgegeben.
Die canAccess()- / checkAccess()-Sperre am Autocomplete-Endpunkt validiert die Token-Integrität (nicht gefälscht, nicht abgelaufen), validiert aber nie die Autorisierung (ist dieser Aufrufer berechtigt, Benutzer zu suchen?). Die Gültigkeit eines CSRF-Tokens ist kein Ersatz für eine Autorisierungsprüfung. Im Vergleich zu getSelectedUsers() im selben Controller, das korrekt Checker::canViewUser() pro Ergebnis aufruft, hat view() keine entsprechende Prüfung.
Ein Python-Skript + Nuclei-Erkennungstemplate sind in diesem Repository enthalten. Das Python-Skript kann nur gegen eine einzelne URL verwendet werden. Wenn Sie mehrere Ziele gleichzeitig testen möchten, verwenden Sie stattdessen nuclei. Beide automatisieren die oben beschriebene zweistufige Kette und gleichen auf bestätigte Benutzerdaten in der Antwort von Schritt 2 ab.
python3 CVE-2026-18110.py https://target.example.com
nuclei -t CVE-2026-18110.yaml -u https://target.example.com
Nur zur Verwendung gegen Systeme gedacht, für deren Test Sie autorisiert sind.
Aktualisieren Sie auf Concrete CMS 9.5.3 oder höher. Der Fix führt eine Autorisierungsprüfung in der Phase der Token-Ausstellung (quickSelect()) ein, konsistent mit der bereits in selectUser() vorhandenen canAccessUserSearch()-Sperre, und fügt eine äquivalente Berechtigungsprüfung innerhalb von view() des Autocomplete-Controllers hinzu, die das Checker::canViewUser()-Muster widerspiegelt, das bereits korrekt von getSelectedUsers() verwendet wird.
Wenn ein sofortiges Upgrade nicht möglich ist, ziehen Sie in Betracht, unauthentifizierten Zugriff auf /ccm/system/panels/ auf Web-Server- oder WAF-Ebene als vorübergehende Maßnahme zu blockieren.
Dieses Repository wird zu Bildungs- und defensiven Sicherheitszwecken veröffentlicht. Alle Tests wurden gegen Systeme unter ausdrücklicher Autorisierung durchgeführt. Die Autoren sind nicht verantwortlich für jeglichen Missbrauch der hierin enthaltenen Informationen oder Tools.