Photo by Kai Pilger on Unsplash

The “Principle of Least Privilege” means that applications and processes should only be granted the privileges that they require to complete their tasks. It is a best practice that lowers the risk of system compromise during an attack.

For example, when an application requires only read access to a file, it should not be granted any write or execute permissions. Because if an attacker hijacks an application that runs with high privileges, the attacker can gain its permissions.

In reality, many applications and services run using high or even root privileges. This is because some systems lack the granular permissions control needed to apply the principle of least privilege. Sometimes, developers and admins forget to apply the best practice. Still, sometimes, developers and admins take a shortcut to avoid dealing with detailed permission control.

Additionally, some applications that are not meant to be run using high privileges do not implement the appropriate safety precautions against attackers. Overprivileged processes thus create a dangerous security weakness that could compromise the entire system.

Example Vulnerabilities

Today, let’s look at three things that attackers can do when they encounter an overprivileged process running as root!

Exploiting a Classic Command Injection

Let’s say that a web application suffers from a classic command injection attack.

<?php

[...]

$file=$_GET['filename'];

system("cat $file"); [...] ?>  The application allows users to read a system file by submitting the filename via a GET request parameter. https://example.com/read?filename=abc.txt  This is a pretty bad vulnerability already. The application lacks any input validation on the system call and enables attackers to execute all kinds of system commands via command injection. https://example.com/read?filename=abc.txt;ls  But what if the web application has root privileges? Then the attacker can do a lot worse because the command injection will also run under root privileges. For example, /etc/passwd is only editable by users with root privileges. The attacker can add themselves as a root user by editing the /etc/passwd file. In this command below, “0” is the UID of the root user, so adding a user with the UID of “0” will give that user root privileges. This command will add a root user with the username “vickie” and an empty password: echo "vickie::0:0:System Administrator:/root/root:/bin/bash" >> /etc/passwd  Exploiting a Database Injection Sometimes, attackers can achieve RCE through a database injection. Many applications and services allow attackers to run system commands through an injection. Let’s look at a PostgreSQL injection for example! If an attacker can gain access to a PostgreSQL database superuser, what can they do? psql --user postgres -h sqlserver  In PostgreSQL 9.3 and above, the “COPY TO/FROM PROGRAM” function allows database superusers to execute code in the context of the database’s operating system user. First, the attacker needs to create a table to hold the system command’s output. > CREATE TABLE cmd_exec(cmd_output text);  Then, they can run the system command via the COPY TO/FROM PROGRAM function. > COPY cmd_exec FROM PROGRAM 'id';  The damage that such an attack can cause is limited if the database runs as a low-privilege user. The attacker might be able to read some files and gain more information about the machine. But if the database is running as root, then things can become much worse. > COPY cmd_exec FROM PROGRAM 'echo "vickie::0:0:System Administrator:/root/root:/bin/bash" >> /etc/passwd';  In addition to not running processes as root, this attack also shows why you shouldn’t allow applications to connect to the database as a superuser. Rather, a separate database user should be created, which only has rights to a particular database or schema. Additionally, that user shouldn’t be allowed to use certain definition commands, such as CREATE and DROP. Exploiting an Arbitrary File Read Let’s say that a path traversal attack exists on this endpoint in a web application. https://example.com/read?filename=abc.txt  An attacker can read files outside of the current directory by using the sequence “../” in the filename parameter. https://example.com/read?filename=../../../../etc/shadow  The /etc/shadow file is a file in Linux systems that contains the hashed passwords of system users. If the web application has the permissions to view the /etc/shadow file, an attacker can utilize the path traversal vulnerability to gain access to this file. Then, the attacker can crack the passwords they found in this file to gain access to privileged users’ accounts on the system. Exploiting Redis Let’s look at another example! If an attacker gains access to an overprivileged Redis instance, they can utilize Redis to escalate their privileges on the system. Let’s dive into how the attack works! Attackers can use Redis to write their RSA public key to the /root/.ssh/authorized_keys file and gain root access through SSH. Let’s say that an attacker was able to gain access to an unprotected Redis server. First, the attacker needs to locally generate an SSH public and private key pair with the ssh-keygen command. Then, they pad the top and bottom of the file with newlines. $ ssh-keygen -t rsa -b 2048
$(echo -e "\n\n"; cat ~/.ssh/id_rsa.pub; echo -e "\n\n") > public.txt  The attacker can then connect to the exposed Redis service to write the key file. $ cat public.txt | redis-cli -h 172.217.9.46 -x set public


Finally, the attacker configures Redis and writes the public key file into the authorized_keys file in /root/.ssh.

$redis-cli -h 172.217.9.46 > config set dir /root/.ssh/ > config set dbfilename "authorized_keys" > save  This will add the attacker’s public key into /root/.ssh/authorized_keys, granting the attacker root access over SSH. The attacker can then log in to the server with their corresponding private key. $ ssh -i ~/.ssh/id_rsa root@172.217.9.46


Remember, Redis should run as the Redis user and not as the root user!

How to Run with Lower Privileges

The three attacks introduced in this post are all introduced by an overprivileged process.

Overprivileged processes can be prevented by carefully configuring permission settings and never using “run as root” as the default solution to permission issues.

Systemd Services

First, you can use “systemd unit files” to run a service as a particular user or group. In Linux, unit files contain configuration directives that will control a service’s behavior. Custom unit files for system-wide services are located in /etc/systemd/system/. And unit files of user packages are located in /lib/systemd/system/.

A unit file is comprised of three sections, Unit, Service, and Install. You can add a “User” or “Group” directive in the “Service” section of the service’s unit file. Doing so will direct the service to run as the user or group.

[Unit]

[Service]
User=
Group=

[Install]


You can read more about unit files here. It is also possible to add additional configuration to an existing systemd unit file using sudo systemctl edit service-name.service, where service-name is the name of the service. This configuration will be merged with the full unit file when the unit is loaded.

It is possible to restart a service using sudo systemctl restart service-name.

Crontabs

In crontabs, you can specify the user that will execute the command after the entry’s time fields.

30 21 * * * USERNAME cd /Users/vickie/scripts/security; ./scan.sh


Nginx + PHP

Web servers commonly run with root privileges because root privileges are needed to be able to run a web server on lower ports, such as 80 and 443. However, before serving files and running scripts, the web server will drop these privileges and run as a lower privileged user, most commonly www-data.

You can change which lower privileged account the web server uses. However, it should never be set to a privileged user. For example, in Nginx, the lower privileged user is defined in the user parameter in the file nginx.conf. In PHP-FPM, the user is defined in the user parameter in the php-fpm.conf file.

Docker Containers

Applications running inside Docker containers will run as root by default. The root user inside the container typically does not have all the capabilities that the root user has on the host, unless the Docker container is started with the --privileged option. However, this user still has more privileges than are needed for many applications. In particular, many “container escape” exploits require the attacker to have root privileges inside the container.

When running a docker container from the command line, you can use the --user option to specify a non-root user for the container:

docker run --user app-user my-docker-image


If you are running the container with docker-compose, you can specify a user in the docker-compose.yml file:

version: "2.0"
services:
my-app:
image: my-docker-image
user: app-user


It is also possible to specify the user in the Docker image itself, by adding a USER directive to the Dockerfile.

USER app-user


In all of these cases, you need to be aware that Docker will look up the user in the /etc/passwd file inside the container. Therefore, if the desired user is not already defined in this file, the container will not be able to run. To fix this, you can create a new entry in the container’s /etc/passwd when building the image, for example by using the adduser command.

RUN adduser --system app-user


If you also configure the user in the Dockerfile with a USER directive then you must put the RUN directive that creates the new user before the USER directive.

Other Security Controls

You can also use security controls such as chroot, seccomp, containers, and VMs to limit the capabilities of a process instead. Chroot limits the application to a modified environment that cannot access files and commands outside that environment. Seccomp limits the system calls that can be made by the application. Containers and VMs isolate the processes running inside the environment from the greater outside system, thus limiting the damage that attackers can cause.

Problems Caused by Lowering Privileges

Sometimes execution will break if an application is not run as root. This might be because the application needs access to a file that it does not have permissions to access, or when applications try to bind to port numbers less than 1024. Ports with numbers less than 1024 are privileged ports that only root users are allowed to access.

To resolve the port issue, you can consider granting the application the Linux capability CAP_NET_BIND_SERVICE, which will grant the application the privilege to bind to a lower numbered port. For file access issues, it’s simply necessary to set the correct file permissions.

Another option is to perform the privileged activity in a helper process or framework, which exposes a smaller attack surface because it does minimal processing on the user-supplied data.

For example, instead of your application binding to a privileged port itself, you can configure your application to bind to an unprivileged port and then set up a reverse proxy server, running with higher privileges, which receives incoming connections and forwards them to your application.

If your application is running as a Docker container, you can use Docker’s port mapping feature instead of configuring a separate reverse proxy. If running directly from the command line, use the --publish option:

docker run --publish 80:12345 my-docker-image --port 12345


If you are running with docker-compose, add a ports section to your docker-compose.yml file:

version: "2.0"
services:
my-app:
image: my-docker-image
user: app-user
command: "my-app --port 12345"
ports:
- "80:12345"


Conclusion

Applications and processes should only be granted the privileges that they require to complete their tasks. Carefully configure process permissions, and you can prevent overprivileged processes from putting your system at risk!