michael orlitzky

End root chowning now (make /etc/init.d great again)

posted 2016-12-27

Running chown or chmod as root is dangerous. Especially in scripts. Cut that shit out.

the problem

Both chown and chmod follow filesystem links. Do you check every file that you chown to verify that it isn't a link? I sure the fuck don't. Symbolic links stand out, but hard links are more or less invisible. To summarize,

So what? Here's what. Most root uses of chown and chmod are exploitable by unprivileged users to gain root.


First, I create a hard link in my home directory:

user $ ln /root/.bashrc /home/mjo/.bashrc

Now I complain to my system administrator that he accidentally messed up the permissions on my ~/.bashrc—which looks just like you'd expect a ~/.bashrc file to look—and could he please chown it to me? It's in my home directory, so what's the worst that could possibly happen?

root # chown mjo:mjo /home/mjo/.bashrc

I told you to cut that shit out. Now I'm root.

user $ ls /root/.bashrc

-rw-r--r-- 2 mjo mjo 1.6K 2016-12-23 10:51 /root/.bashrc

If you think that example is far-fetched, did you know that tar archives can contain absolute symlinks?

root # tar -xf stupid-webapp-1.2.3.tar.xz

root # chown apache:apache stupid-webapp-1.2.3/*

Ha ha, the tarball contained a symlink! You're fucked.

root # ls /etc/passwd

-rw-r--r-- 1 apache apache 1.6K 2016-12-25 14:21 /etc/passwd

root # ls stupid-webapp-1.2.3/

total 8.0K

lrwxrwxrwx 1 mjo mjo 11 2016-12-26 12:53 CHANGES -> /etc/passwd

-rw-r--r-- 1 apache apache 8 2016-12-26 12:53 index.php

-rw-r--r-- 1 apache apache 8 2016-12-26 12:53 README.txt

Now I know what you're thinking…

You, chewing on your keyboard
Okay, I shouldn't run chown as root. But how about if I run chown automatically as root all the time as part of my init scripts? That's cool, right?
It is not cool. Calling chown in an init script is the most bad thing, because it happens repeatedly and in a predictable place.


Go peep CVE-2016-8641 for Nagios, whose init script calls chown as root. The nagios user owns the /var/nagios directory, and the Nagios init script calls chown on /var/nagios/nagios.log every time Nagios is started. To paraphrase,

start() {
  chown nagios:nagios /var/nagios
  chown nagios:nagios /var/nagios/nagios.log

The problem here is that the nagios user can replace /var/nagios/nagios.log with a symlink, because he owns the whole directory.

nagios $ ln --symbolic --force /etc/passwd /var/nagios/nagios.log

The next time Nagios is started, the init script will give the nagios user ownership of the target of his symlink. Oops. The same problem exists with hard links. If you call chown with --no-dereference, then it won't follow a symlink, so you might think that doing so is safe. Cut that shit out. The “fix” for CVE-2016-8641 changes all of the chown calls to include the --no-dereference flag. The end result is—paraphrasing again,

start() {
  chown nagios:nagios /var/nagios
  chown --no-dereference nagios:nagios /var/nagios/nagios.log

See a problem? The fix only works for symlinks. The nagios user can just as easily do,

nagios $ ln --force /etc/passwd /var/nagios/nagios.log

The next time Nagios is started, the nagios user will be given ownership of the target of the hard link. Hard links can only point to a file living on the same filesystem, but, if you try hard enough, you can usually find something nasty to own.

As a variation on a theme, chown will not follow symlinks when it is operating recursively. Yet this is equally stupid,

start() {
  chown --recursive nagios:nagios /var/nagios

because chown will follow hard links recursively. The same trick,

nagios $ ln /etc/passwd /var/nagios/whatever.log

will get nagios ownership of /etc/passwd the next time Nagios is started.

what to do: as a system administrator

Just cut that shit out. As root, it's only safe to call chown/chmod in a directory that root controls, and on a path that root controls.

And it rarely needs to be done. A common misuse is to configure some new thing—a new website, a new daemon, a new home directory, whatever—as root, and then chown everything to the account that will actually use it. A better means to that end is to create only what you need as root, and then switch to the other user to do the rest.

For example, this is bad:

root # mkdir /home/derp

root # cp /etc/skel/.bashrc /home/derp/.bashrc

root # echo -e "[user]\n name = Derp" > /home/derp/.gitconfig

root # chown -R derp:derp /home/derp

This is a much safer way to accomplish the same thing:

root # mkdir /home/derp

root # chown derp:derp /home/derp

root # su - derp

derp $ cp /etc/skel/.bashrc ~/

derp $ echo -e "[user]\n name = Derp" > ~/.gitconfig

In both cases you need to ensure that /home is owned by root:root and is not world-writable. When you do need to chown or chmod as root, be careful, and implement the what to do: as a user precautions.

what to do: as a developer

Quit using chown and chmod in your init scripts.

OpenRC init scripts should be using the checkpath helper, which doesn't follow links of any kind. Instead of this,

mkdir /var/lib/foo
chown root:somebody /var/lib/foo
chmod g+w /var/lib/foo

You should be doing this:

checkpath --directory -o root:somebody -m775 /var/lib/foo

The checkpath helper is documented in the openrc-run man page. Generally, the files that are created by a daemon (in a directory that it can write to) will already have the correct ownership and permissions. If not, the umask of the process can be changed: don't use a chmod shotgun as a workaround. You can use checkpath to create and set permissions on files as well; but generally, if you have to “fix” a bunch of permissions whenever a daemon starts, you're doing it wrong.

If some file needs to be created once (with certain permissions), don't do it in the init script. This is bad:

start() {
  if [ ! -f /etc/foo/secret.key ]; then
    dd if=/dev/urandom bs=1 count=1024 > /etc/foo/secret.key
    chmod 640 /etc/foo/secret.key

There's an exploit there if /etc/foo is writable by anyone other than root. Instead, document the creation of /etc/foo/secret.key as part of your installation procedure, and then don't run chmod automatically as root. Gentoo packages would be better off creating that file in the ebuild's pkg_config phase. If you really really really really really want to create that key in the init script, use umask to avoid calling chmod:

start() {
  if [ ! -f /etc/foo/secret.key ]; then
    local orig_umask=$(umask)
    umask 027
    dd if=/dev/urandom bs=1 count=1024 > /etc/foo/secret.key
    umask $orig_umask

But seriously, if it needs to be done once, it doesn't belong in an init script. There's still a race condition in the version above. Just don't even try.

You might think that you want to make your daemon user configurable. It sounds good; all you need is a variable FOO_USER=foo, and then you can launch the daemon as that user. But for that to work, you have to change ownership of the daemon's files every time the value of the FOO_USER variable changes. And since you have no way to know when that is, you must fiddle with the ownership constantly:

start() {
  # if FOO_USER is different than the last time we started, we
  # need to fix the permissions on its runtime directory.
  chown --recursive $FOO_USER:$FOO_USER /var/lib/foo

You already know what I'm going to tell you: cut that shit out. But how can we make things work if the FOO_USER variable can change at any time? Sorry: you can't. Might as well get over it. If some dude wants to dick around with the users/groups that you've given him, then he's on his own to fix the permissions—he'll only have to do it once. Doing it automatically is not an option: the FOO_USER above can put hard links in /var/lib/foo and take over the system.

what to do: as a user

As a user, you mainly have one avenue to prevent these sorts of attacks: prevent the links from being created in your kernel. The behavior of filesystem links is specified by POSIX and changing it now would be backwards-incompatible, so you shouldn't hold your breath waiting for this to get fixed anywhere else.

The vanilla Linux kernel

The vanilla Linux kernel has two sysctl options that can be used to prevent certain filesystem links from being created. You should go enable both of these right now. If your kernel doesn't have them, they were added in v3.6 (2012), so frickin upgrade already.

When enabled, the fs.protected_hardlinks sysctl disallows hard-linking to shit you don't have any business hard-linking to: files you don't own, and can't read/write. Likewise, the fs.protected_symlinks sysctl will disallow symlinks from a sticky world-writable directory unless you control the target. The Linux kernel documentation explains the same thing with less cussing.

Turn them both on permanently by adding the following to your /etc/sysctl.conf:

fs.protected_hardlinks = 1
fs.protected_symlinks = 1

To enable them now, before you reboot, do

root # sysctl --write fs.protected_hardlinks=1

root # sysctl --write fs.protected_symlinks=1

The Gentoo Linux kernel

The Gentoo kernel enables fs.protected_hardlinks and fs.protected_symlinks by default. Good for y'all.

a grsecurity kernel

The folks at grsecurity maintain a kernel patchset that fixes… a lot of things. If you set CONFIG_GRKERNSEC_LINK=y, then it enacts the same linking restrictions that fs.protected_hardlinks and fs.protected_symlinks together would provide. It does that via another sysctl, kernel.grsecurity.linking_restrictions, which is enabled by default.

filesystem segregation

Hard links only work between files on the same filesystem. In theory, if you could mount each user's home (or working, for daemons) directory on a separate filesystem, then the hard-link attack would be mitigated. That's too impractical to be of any use, however.