On November 03, SaltStack released a security patch for Salt to fix three critical vulnerabilities. Two of these fixes were in response to five bugs originally reported through the ZDI program. These bugs can be used to achieve unauthenticated command injection on a system running the affected Salt application. ZDI-CAN-11143 was reported to the ZDI program by an anonymous researcher, while the remaining bugs are variants of ZDI-CAN-11143 discovered by me. In this blog, we will look into the root cause of these bugs.
The vulnerabilities affect the
rest-cherrypy netapi module of the application. The
rest-cherrypy module provides REST APIs for Salt. The module is dependent on the
CherryPy Python module and is not enabled by default. To enable the
rest-cherrypy module, the master configuration file
/etc/salt/master must contain the following lines:
In this case, the “/run” endpoint is important. It is used to issue commands via the
salt-ssh subsystem. The
salt-ssh subsystem allows the execution of Salt routines using Secure Shell (SSH).
A POST request sent to the “/run” API will invoke the
POST() method of the
salt.netapi.rest_cherrypy.app.Run class, which eventually calls the
run() method of
As shown above, the
run() method validates the value of the
client parameter. Valid values of the
client parameter are “local”, “local_async”, “local_batch”, “local_subset”, “runner”, “runner_async”, “ssh”, “wheel”, and “wheel_async”. After validating the
client parameter, it checks for the presence of the
eauth parameter in the request. Interestingly, the method doesn’t validate the value of the
eauth parameter. Because of this, an arbitrary value of the
eauth parameter can pass this check. Once this check is passed, the method invokes a corresponding method depending on the value of the
The vulnerability occurs when the value of the client parameter is “ssh”. In this case, the
run() method calls the
ssh() method. The
ssh() method executes
ssh-salt commands synchronously by calling the
cmd_sync() method of the
salt.client.ssh.client.SSHClient class, which eventually results in the
_prep_ssh() method being called.
_prep_ssh() function sets parameters and initializes the SSH object.
The vulnerable request to trigger this vulnerability is as follows:
In this, the value of the
client parameter is “ssh” and the vulnerable parameter is
ssh_priv. Internally, the
ssh_priv parameter is used during SSH object initialization, as shown below:
The value of the
ssh_priv parameter is used as an SSH private file. If the file represented by the
ssh_priv value doesn’t exist, the
gen_key() method of
/salt/client/ssh/shell.py is called to create the file and
ssh_priv is passed to the method as the
path argument. Basically, the
gen_key() method generates public and private RSA key pair and stores it in a file defined by the
The method shown above indicates that
path is not sanitized, and it is used in a shell command to create an RSA key pair. If
ssh_priv contains command injection characters, it is possible to execute user-controlled commands while executing the command by the
subprocess.call() method. This allows an attacker to run arbitrary commands on the system running the Salt application.
On further investigation of the SSH object initialization method, it can be observed that multiple variables are set to the user-controlled HTTP parameters’ values. Later on, these variables are used as arguments in a shell command to execute an SSH command. Here, the
ssh_options variables are vulnerable as shown below:
_update_targets() method sets the
user variable, which is dependent on the
ssh_user value. If the value of the
tgt HTTP parameter is in “[email protected]” format, “username” is assigned to the
user variable. Otherwise, the value of
user is set by the
ssh_user parameter. The
ssh_options values are defined by
ssh_options HTTP parameters, respectively.
After initializing the SSH object, the
_prep_ssh() method spawns a child process via
handle_ssh() to eventually execute the
exec_cmd() method of
exec_cmd() first calls the
_cmd_str() method to create a command string without any validation. Afterwards, it calls
_run_cmd() to execute the command by invoking the system shell explicitly. This treats command injection characters as shell metacharacters rather than the arguments of the command. Execution of this crafted command string can lead to the arbitrary command injection condition.
SaltStack released patches to fix the command injection and authentication bypass vulnerabilities. In doing so, they assigned them CVE-2020-16846 and CVE-2020-25592, respectively. The patch for CVE-2020-16846 addressed the vulnerability by disabling the system shell when executing commands. The disabling of the system shell means that shell metacharacters will be treated as part of the arguments of the first command.
The patch for CVE-2020-25592 addressed the vulnerability by adding validation for the
token parameters. This allows only valid users to access the
salt-ssh functionality via the
rest-cherrypy netapi module. These were the first SaltStack bugs to come through the ZDI program, and they were interesting to work on. We hope to see more in the future.
Detailing SaltStack Salt Command Injection Vulnerabilities