Subject: Hardlink mitigation limits

From: Nathan Nye <[email-masked]>
Date: Tue, 23 Mar 2021 14:18:14 -0400

Hello,

I'm sharing here for future reference why protected hardlinks alone did 
not mitigate the recent LPE security advisory, pre-patch:

"The reasons why are lines 2633 and 2637 of nix/libstore/build.cc:

  * https://git.savannah.gnu.org/cgit/guix.git/tree/nix/libstore/build.cc#n2633
  * https://git.savannah.gnu.org/cgit/guix.git/tree/nix/libstore/build.cc#n2637

When a package fails to build and the keep failed flag is set 
(-K/--keep-failed), it runs a recursive chown on the build directory 
(which is writable following guixbuilder01 changing the permissions to 
777). It starts at the top level and chowns downwards.

The first important thing to notice here is that at any point (even 
pre-chown) the build user has been compromised. The build user can write 
a SUID /bin/sh to the build path, and because a normal user can traverse 
into the directory before and during the chown, they can run a SUID 
shell (allowing them to become guixbuilder01 even after the build user 
processes are terminated). Becoming the build user allows multiple paths 
to privilege escalation, but in this scenario we have faster ways of 
becoming root.

Moving on to getting root, we're choosing not to use a hardlink to show 
why it isn't necessary. Instead, we create a directory under the build 
directory with thousands of sequentially named files, the final entry 
being "passwd" or "shadow". Then we terminate the build and watch for 
the first entry to be chowned to our user ID (possibly with the inotify 
API). This way, we have opened a lengthy window of time where it is 
enumerating over a list of file paths in our chosen directory and 
chowning each of them. Now we can execute our TOCTOU race condition 
vulnerability.

At the time of check (TOC), the guix-daemon has a list of file paths to 
chown under what it assumes is a regular directory (because it ran 
S_ISDIR on the directory). But we can swap out the directory from under 
it with a symlink to /etc (most efficiently with renameat2() and using 
the RENAME_EXCHANGE flag to atomically exchange the paths). At the time 
of use (TOU) lchown() only checks if the file /itself/ that is being 
chowned is a symlink, not if the path components are, as can be 
demonstrated with Python:

$ mkdir td;touch td/tf;python3 -c 'import os;os.lchown("/home/example/td/tf", 1000, 4)';ls -lahtrd td td/tf
-rw-rw-r-- 1 example adm       0    Mar 19 19:20 td/tf
drwxrwxr-x 2 example example   4.0K Mar 19 19:20 td
$ rm -rf td
$ mkdir td; ln -s td td2;touch td2/tf;python3 -c 'import os;os.lchown("/home/example/td2/tf", 1000, 4)';ls -lahtrd td2 td2/tf
lrwxrwxrwx 1 example example 2 Mar 19 19:21 td2 -> td
-rw-rw-r-- 1 example adm     0 Mar 19 19:21 td2/tf

So lchown can blindly chown /etc/passwd to our user by following the 
directory symlink and subsequently verifying that passwd itself is not a 
symlink. I hope this explains the TOCTOU race condition and why 
protected hardlinks help (forcing an attacker to get root using this 
race condition), but they are not a solution to the problem (alone)."

- Nathan