Detailing SaltStack Salt Command Injection Vulnerabilities
November 24, 2020 | KP ChoubeyOn 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 Vulnerability
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 salt.netapi.NetapiClient
:
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 token
or eauth
parameter in the request. Interestingly, the method doesn’t validate the value of the token
or eauth
parameter. Because of this, an arbitrary value of the token
or eauth
parameter can pass this check. Once this check is passed, the method invokes a corresponding method depending on the value of the client
parameter.
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.
The _prep_ssh()
function sets parameters and initializes the SSH object.
ZDI-CAN-11143
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 path
argument.
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 user
, port
, remote_port_forwards
, and ssh_options
variables are vulnerable as shown below:
The _update_targets()
method sets the user
variable, which is dependent on the tgt
or ssh_user
value. If the value of the tgt
HTTP parameter is in “username@localhost” format, “username” is assigned to the user
variable. Otherwise, the value of user
is set by the ssh_user
parameter. The port
, remote_port_forwards
, and ssh_options
values are defined by ssh_port
, ssh_remote_port_forwards
, and 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 salt.client.ssh.shell.Shell
class.
As shown, 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.
Conclusion:
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 eauth
and 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.