Submit #905111: Formalms forma.lms 4.1.43 SQL Injectioninfo

TitelFormalms forma.lms 4.1.43 SQL Injection
Beschreibungforma.lms 4.1.43 contains an SQL injection vulnerability in the multi-user-selector AJAX endpoint. Any authenticated user, including an ordinary student account with no administrative privileges, can inject arbitrary SQL into several database queries by controlling the name of a DataTables filter column. This allows reading the entire contents of the application database, including the password hashes of every user. AFFECTED ENDPOINT HTTP POST to: /appCore/ajax.adm_server.php?r=adm/userselector/getData Handled by UserselectorAdmController::getDataTask(), file html/appCore/controllers/UserselectorAdmController.php line 174. ROOT CAUSE The endpoint accepts parameters in DataTables server-side processing format. The value of columns[i][name] is taken directly from the request and used as the ARRAY KEY of the column filter map, so it undergoes no filtering or validation. This happens in three data selectors: html/lib/Selectors/Multiuserselector/DataSelectors/UserDataSelector.php line 36 html/lib/Selectors/Multiuserselector/DataSelectors/GroupDataSelector.php line 29 html/lib/Selectors/Multiuserselector/DataSelectors/RoleDataSelector.php line 30 The code is: foreach ($columns as $column) { if ($column['search']['value'] != '') { $columnsFilter[$column['name']] = $column['search']['value']; } } That key is later concatenated into SQL as an UNQUOTED IDENTIFIER, in 13 distinct locations: html/appCore/models/UsermanagementAdm.php lines 405, 441, 481, 517, 553, 848, 983 html/appCore/models/GroupmanagementAdm.php lines 106, 225, 247 html/appCore/models/FunctionalrolesAdm.php lines 149, 230, 277 The vulnerable pattern is: $query .= ' AND ( u.' . $columnName . ' LIKE "%' . $columnValue . '%" )'; The application applies addslashes() plus HTMLPurifier to all request input during the boot step BOOT_INPUT (html/lib/lib.filterinput.php). This is ineffective here for two independent reasons. First, the injected value is an SQL IDENTIFIER, not a string literal: it is not enclosed in quotes, so an attacker does not need quote characters to alter the query, and escaping quotes provides no protection. Note that the filter VALUE is enclosed in double quotes and is therefore protected; only the column NAME is exploitable. Second, the controller does not read the sanitized superglobals. It reads from the Symfony HttpFoundation Request object, which is created by Request::createFromGlobals() during boot step BOOT_REQUEST (constant value 5, html/base.php line 95), whereas sanitization happens in BOOT_INPUT (constant value 11, html/base.php line 101). Because the superglobals are snapshotted before sanitization runs, the values read at html/appCore/controllers/UserselectorAdmController.php line 44 are completely unsanitized. PRIVILEGE REQUIRED Any authenticated session. UserselectorAdmController overrides the base class initializer without calling parent::init() and performs no permission check of its own, so the administrative permission gate is never executed. This missing authorization is reported separately. The only control on the request path is the anti-CSRF session token (parameter authentic_request, or header X-Signature), which every authenticated user legitimately possesses and can read from the source of their own pages. This was confirmed by testing: a student account with no administrative privileges receives HTTP 200 and a valid JSON response from this endpoint. The exploitable branch requires the global search parameter to be empty and the per-column search value to be non-empty; both conditions are trivially met. PROOF OF CONCEPT Validated on a live forma.lms 4.1.43 instance running MariaDB 10.11 and PHP 8.4, authenticated as a student account with no administrative privileges. Request: POST /appCore/ajax.adm_server.php?r=adm/userselector/getData Body (URL-encoded): dataType=group op= search[value]= columns[0][name]=groupid) OR (SELECT SLEEP(3)) OR (g.groupid columns[0][search][value]=x authentic_request=<session anti-CSRF token> Measured response times: Baseline, columns[0][name]=groupid 0.013 seconds Injection, payload above 36.0 seconds The query actually executed, captured from the MySQL general query log, was: SELECT COUNT(*) FROM core_group as g WHERE g.hidden = 'false' AND g.type <> 'course' AND ( g.groupid) OR (SELECT SLEEP(3)) OR (g.groupid LIKE "%x%" ) The payload closes the parenthesis opened by the query template, injects an uncorrelated scalar subquery that MySQL evaluates once, and re-opens a parenthesis so that the trailing LIKE clause and closing parenthesis of the template remain syntactically valid. The same vector is reachable against three datasets by varying the dataType parameter: user, group and role, each mapping to its own set of sinks listed above. IMPACT An authenticated user holding the lowest privilege level on the platform can extract the full contents of the database via blind SQL injection, including all rows of core_user and therefore every user's password hash.
Quelle⚠️ https://github.com/lorenzobruno7/advisory_formalms_sql/tree/main
Benutzer Brunlorenz (UID 95709)
Einreichung25.07.2026 14:18 (vor 2 Monaten)
Moderieren23.09.2026 18:22 (2 months later)
StatusAkzeptiert
VulDB Eintrag409034 [Forma LMS bis 4.1.43 Multi-User-Selector AJAX Endpoint getData getDataTask Name SQL Injection]
Punkte20

Interested in the pricing of exploits?

See the underground prices here!