Detailing SaltStack Salt Command Injection Vulnerabilities

November 24, 2020 | KP Choubey

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 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.

You can find me on Twitter @nktropy, and follow the team for the latest in exploit techniques and security patches.