Moodle 5.2.3 - High - Remote - SQLi - enrol_database concatenates course idnumber into SQL
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · moodle-enrol-database-sqli
Moodle LMS 5.2.3 - Moodle
Class: SQLi Reach: Remote
enrol_database::db_get_sql still slashes and concatenates. MSA-26-0005 / MDL-88138 parameterized auth_db as Execute($sql, [$value]). This plugin did not copy that shape. Defaults in settings: dbsybasequoting=0, localcoursefield=idnumber. The sink is the external enrolment database. Moodle core $DB is not on this path.
An editing teacher on a site with enrol_database enabled sets the course idnumber. Cron interpolates it. Extra rows come back from the enrol DB.
| ID | no CVE yet |
| CWE | CWE-89 |
| CVSS | High: 8.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| Product | Moodle |
| Affected | 5.2.3 tag v5.2.3 (344232c15336c71b80f9aca8359ce0e0a9f3d116). Plugin enrol_database must be on. |
| Auth | editing teacher (course idnumber, default local map) |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 only |
An editing teacher on a site with enrol_database enabled injects SQL on the external enrolment database by setting the course idnumber. Cron sync_enrolments then runs that SQL.
| Privilege | Method / sink | Recon / integrity / availability | Lab-proven or inferred |
|---|---|---|---|
| Splice a SQL fragment into the external enrol SELECT | Cron sync_enrolments -> db_get_sql($table, array($coursefield=>$course->mapping), ...) -> $extdb->Execute($sql) | Integrity of the query string | Lab-proven. Concat SQL is WHERE courseid = 'LABCOURSE\'' OR 1=1 -- ' |
| Read other courses' enrol rows on that external DB | Same SELECT after the string closes | Recon | Lab-proven. Clean: 1 row teacher-local. Inject: 2 rows including MOODLE-ENROL-DATABASE-SQLI-WITNESS |
| Skip the query when the plugin is off | Same gadget, plugin=off | n/a | Lab-proven closed. executed=false, empty SQL, 0 rows |
| Hold the same payload as a bound parameter | auth_db leftover of MSA-26-0005: Execute($sql, [$value]) | n/a | Lab-proven closed. style=param returned 0 rows, SQL contains ? |
| Enrol / unenrol / role-assign Moodle users named in extra rows | After cron applies requestedenrols | Integrity of enrolments | Inferred. Lab stops at extra rows on the enrol DB |
| FILE / INTO OUTFILE as the enrol-DB user | Same sink if that account has FILE | Integrity / availability of that DB host | Inferred. Not demonstrated. Not OS LPE |
| Student-controlled path | Only if admin maps localuserfield=email (or similar) | n/a | Inferred. Default path is teacher idnumber |
Closed on this leftover: Moodle core $DB; plugin disabled (no query); unauthenticated access; OS shell; stacked queries (this mysqli gadget is single-statement). Default Moodle without enrol_database is not this bug.
Same pin leftovers: Blackboard img file read, grades feedback IDOR, hidden grade items.
I pinned moodle/moodle v5.2.3 after MSA-26-0042/0043. MSA-26-0005 closed auth_db with a placeholder. I opened enrol_database because it talks to the same kind of external DB and still has db_addslashes.
dbsybasequoting defaults to 0, so only ' is doubled. A backslash-quote in the idnumber closes the MySQL string.
Wrong turns already recorded: treating Moodle core $DB as the sink (it is not); calling this OS LPE because FILE exists as a MySQL privilege (not demonstrated, not this leftover); student email as the default map (admin has to change localuserfield). A reverse shell. Theatre.
Faithful gadget of the pin builders, not a full Moodle PHP app. php:8.3-cli serves the copied db_get_sql / db_addslashes / db_encode. mysql:8.0 holds two enrol rows.
python3 moodle-enrol-database-sqli-Abraxas-Labs.py
or
cd lab
./run.sh
Target only http://127.0.0.1:19570. Compose project moodle-enrol-database-sqli. Stack tears down after the run.
SUCCESS moodle-enrol-database-sqli how=gadget plugin-off=no-query clean-rows=1 inject-rows=2 sql-concat=yes param-rows=0 extra=yes sink=enrol-db MOODLE-ENROL-DATABASE-SQLI-WITNESS
Copy auth_db's parameterized queries (MDL-88138). Pass the mapped course / user value as Execute($sql, [$value]). Do not concatenate db_addslashes into '$value'. Add a test that a quote in course idnumber does not return rows from other courses.
lib.php (db_get_sql, db_addslashes, sync_enrolments)auth_dbGNU Affero GPL v3.0. See LICENSE. Loopback lab only. No warranty.