michael orlitzky

End root chowning now (make pkg_postinst great again)

posted 2017-09-29

Running chown or chmod as root is dangerous. Especially in ebuilds, when the target is on the live filesystem. Cut that shit out.

(This article is targeted at Gentoo developers.)

the problem

In the last “end root chowning” article, I convinced you not to call chown and chmod in your init scripts. Well, the same problem exists in the pkg_postinst phase—and to a lesser extent the pkg_config phase—of your ebuilds.

The following (more or less) appears in mail-filter/amavisd-new-2.11.0-r3.ebuild:

pkg_postinst() {
  chown root:amavis "/etc/amavisd.conf"
  chown -R amavis:amavis "/var/amavis"
}

That code is calling chown on every path under (and including) /var/amavis whenever the amavisd-new package is upgraded or reinstalled. That can be exploited by the amavis user (or anyone in the amavis group) to gain root. After amavisd-new is installed, the amavis user owns /var/amavis, and he can create anything he likes in that directory. If he creates a hard link inside /var/amavis pointing to a root-owned file, then the next time that amavisd-new is (re)installed or upgraded, chown will give ownership of the hard link's target to the amavis user. From there it's easy to gain full root access.

Seeing is believing, so believe:

root # emerge --oneshot amavisd-new

root # su --shell /bin/sh --command "ln /etc/passwd /var/amavis/x" amavis

root # emerge --oneshot amavisd-new

root # ls -l /etc/passwd

-rw-r--r-- 1 amavis amavis 3.0K 2017-09-22 11:04 /etc/passwd

The same problem exists with the pkg_preinst phase, and both are extremely dangerous because they get run every time the package is installed. But, the pkg_config phase can be abused too. If pkg_config calls chown recursively and if the user happens to run the phase twice, then the same exploit is possible. For a concrete example, consider net-analyzer/munin-2.0.33-r1.ebuild, which does

pkg_config() {
  ...
  # generate one rsa (for legacy) and one ecdsa (for new systems)
  ssh-keygen -t rsa -f /var/lib/munin/.ssh/id_rsa -N '' \
    -C "created by portage for ${CATEGORY}/${PN}" || die
  ssh-keygen -t ecdsa -f /var/lib/munin/.ssh/id_ecdsa -N '' -C \
    "created by portage for ${CATEGORY}/${PN}" || die
  chown -R munin:munin /var/lib/munin/.ssh || die
  ...
}

You can easily verify that the same exploit works, should the user happen to configure the package twice:

root # emerge --oneshot munin

root # emerge --config munin

root # su --shell /bin/sh --command "ln /etc/passwd /var/lib/munin/.ssh/x" munin

root # emerge --config munin

root # ls -l /etc/passwd

-rw-r--r-- 1 munin munin 3.0K 2017-09-22 17:19 /etc/passwd

tl;dr for developers

Cut that shit out. Calling chown or chmod recursively on the live filesystem is, in and of itself, a security hazard. Even if your ebuild creates /var/foo, you have no idea what lives in /var/foo during pkg_postinst, and you have no business fiddling with the owner and permissions of things that don't belong to you. Maybe I hide my pornography in /var/amavis with mode 0600 and owner:group root:root. Your ebuild shouldn't make that stuff public, trust me. Figure out a way to not do it.

tl;dr for users

As a user, you defend against this the same way you did last time.

cause one

You'll see people calling chown and chmod in those phase functions for a few different reasons. The first reason is “fuck it, let's change the ownership of everything just in case that's the way it's supposed to be.” For example, app-misc/uptimed-0.4.0-r1.ebuild tries to “fix” the permissions in /var/spool/uptimed for absolutely no reason:

src_install() {
  ...
  keepdir /var/spool/uptimed
  fowners uptimed:uptimed /var/spool/uptimed
  ...
}

pkg_postinst() {
  einfo "Fixing permissions in /var/spool/${PN}"
  chown -R uptimed:uptimed /var/spool/${PN}
  ...
}

solution one

Don't fucking do that.

cause two

The second, less absurd reason is because you've created something as root, but want it to be owned by somebody else. For example, take app-admin/logcheck-1.3.18.ebuild:

src_install() {
  ...
  keepdir /var/lib/logcheck
  ...
}

pkg_postinst() {
  chown -R logcheck:logcheck ... /var/lib/logcheck || die
  ...
}

solution two

If you create something as root during installation and later change its ownership, then you should do so in ${D} with fowners, and not on the live filesystem:

src_install() {
  ...
  keepdir /var/lib/logcheck
  fowners logcheck:logcheck /var/lib/logcheck
  ...
}

cause three

If you're creating something on the live filesystem (outside of ${D}) and need to change its owner, then the previous solution won't work. The aforementioned pkg_config phase from net-analyzer/munin-2.0.33-r1.ebuild is a good example of this:

pkg_config() {
  ...
  # generate one rsa (for legacy) and one ecdsa (for new systems)
  ssh-keygen -t rsa -f /var/lib/munin/.ssh/id_rsa -N '' \
    -C "created by portage for ${CATEGORY}/${PN}" || die
  ssh-keygen -t ecdsa -f /var/lib/munin/.ssh/id_ecdsa -N '' -C \
    "created by portage for ${CATEGORY}/${PN}" || die
  chown -R munin:munin /var/lib/munin/.ssh || die
  ...
}

The ${D} variable would be meaningless here, because the package is already installed.

solution three

If you're creating a path as root after installation and then trying to give it away to an unprivileged user, then there's usually a better way: create the path as the unprivileged user in the first place. The munin user is perfectly capable of running ssh-keygen himself, and /var/lib/munin is his home directory, so everything will work out:

pkg_config() {
  ...
  # generate one rsa (for legacy) and one ecdsa (for new systems)
  su --shell /bin/sh --command "ssh-keygen -t rsa -f ..." munin
  su --shell /bin/sh --command "ssh-keygen -t ecdsa -f ..." munin
  ...
}

Now everything is created with the correct ownership and permissions, and we don't have to chown or chmod anything. As part of the fix for Gentoo bug #630822, commit b19f619 serves as another good example of this strategy.

cause four

There is one rare situation where a package legitimately wants to mess with the live filesystem. If an earlier version of a package installed something with the wrong ownership or permissions, then a later version of the package can call chown or chmod on the live filesystem to fix them; the package manager won't alter the pre-existing ownership or permissions otherwise. One example is net-vpn/peervpn-0.044-r4.ebuild:

pkg_preinst() {
  if ! has_version '>=net-vpn/peervpn-0.044-r4' && \
     [[ -d ${EROOT}etc/${PN} &&
        $(find "${EROOT}etc/peervpn" ! -user root -print) ]]; then
    ewarn "Tightening '${EROOT}etc/${PN}' permissions for bug 629418"
    chown -R root:${PN} "${EROOT}etc/${PN}" || die
    chmod -R g+rX-w,o-rwx "${EROOT}etc/${PN}" || die
  fi
}

solution four

This is a tough one. If you absolutely must fix the permissions in your ebuild, then heed the following warnings:

graveyard